ups国际快递网站建设,教育机构网站建设公司,山西省建设局网站,中国机械加工网1717可能有些人会觉得这篇似曾相识#xff0c;没错#xff0c;这篇是由原文章进行二次开发的。 前阵子有些事情#xff0c;但最近看到评论区说原文章最后实现的是单模块的验证#xff0c;由于过去太久也懒得验证#xff0c;所以重新写了一个完整的可以跑得动的一个。 OK#… 可能有些人会觉得这篇似曾相识没错这篇是由原文章进行二次开发的。 前阵子有些事情但最近看到评论区说原文章最后实现的是单模块的验证由于过去太久也懒得验证所以重新写了一个完整的可以跑得动的一个。 OK回到正题以下是真正对应的微服务多模块的一个方法使用到的技术有基于微服务的SpringbootSecurityRedisGatewayOpenFeignNacosJWT。
对使用到的微服务技术进行在项目中的说明
Security负责登录验证文章中没有实现授权在过滤器中直接返回null如果想实现授权可以在返回null的地方添加授权信息类似ROLE_ADMIN同时在Security的配置文件那里添加授权信息即可。
Redis负责缓存token跟用户数据。
Gateway对前端提供的接口由它多个模块进行接口调用。
OpenFeign提供给security查询数据库中的用户信息。
Nacos注册服务中心注册服务的信息使OpenFeign可以调用其他服务模块。
注意虽然是原文章的二次编写但是很多都不同建议直接跟着这篇走。 目录
1.项目结构
2.Common模块
pom.xml
2.1 RedisConfig
2.2 RedisUtil
2.3 ResponseUtil
2.4 TokenUtil 2.5 CorConfig
3.model模块
3.1 pom
3.2 User 3.3 UserFeign
4.service模块 4.1 目录结构编辑 4.2 service_user模块 4.2.1 pom.xml 4.2.2 application.yml 4.2.3 Service_UserApp启动类
4.3 其他service模块
5.spring_security模块
5.1 pom
5.2 DiyUserDetailsUserDetails
5.3 WebSecurityConfigWebSecurityConfigurerAdapter
5.4 TokenOncePerRequestFilterOncePerRequestFilter
5.5 LoginAuthenticationEntryPointAuthenticationEntryPoint
5.6 LoginInFailHandlerAuthenticationFailureHandler
5.7 LoginInSuccessHandlerAuthenticationSuccessHandler
5.8 LogOutSuccessHandlerLogoutSuccessHandler
5.9 NothingAccessDeniedHandlerAccessDeniedHandler 5.10 MyUserDetailServiceUserDetailsService
6.gateway模块
6.1 pom
6.2 application.yml
7.测试 1.项目结构 涉及的模块有
1commonRedis配置文件、Redis工具、Token工具、返回给前端信息的工具即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil
2gateway
3model实体类Feign的客户端
4service用户模块、课程模块
5spring_securitysecurity的过滤器跟配置文件。
下面小编将全部一一介绍并且源码展示出来。
2.Common模块
pom.xml !--springboot_redis缓存框架 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactId/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdcom.github.xiaoymin/groupIdartifactIdknife4j-spring-boot-starter/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactId/dependencydependencygroupIdcom.goyes/groupIdartifactIdmodel/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependency
2.1 RedisConfig
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import javax.annotation.Resource;
/** Redis配置* 解决redis在业务逻辑处理层上不出错缓存序列化问题* */
Configuration
EnableCaching
public class RedisConfig extends CachingConfigurerSupport {ResourceRedisConnectionFactory redisConnectionFactory;Beanpublic RedisTemplateString,Object redisTemplate(){RedisTemplateString,Object redisTemplatenew RedisTemplate();redisTemplate.setConnectionFactory(redisConnectionFactory);
//Json序列化配置//1、String的序列化StringRedisSerializer stringRedisSerializernew StringRedisSerializer();// key采用String的序列化方式redisTemplate.setKeySerializer(stringRedisSerializer);// hash的key也采用String的序列化方式redisTemplate.setHashKeySerializer(stringRedisSerializer);//2、json解析任意的对象Object,变成json序列化Jackson2JsonRedisSerializerObject serializernew Jackson2JsonRedisSerializerObject(Object.class);ObjectMapper mappernew ObjectMapper(); //用ObjectMapper进行转义mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);//该方法是指定序列化输入的类型就是将数据库里的数据按照一定类型存储到redis缓存中。mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);serializer.setObjectMapper(mapper);// value序列化方式采用jacksonredisTemplate.setValueSerializer(serializer);// hash的value序列化方式采用jacksonredisTemplate.setHashValueSerializer(serializer);return redisTemplate;}
}
2.2 RedisUtil
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;Component
public class RedisUtil {Autowiredprivate StringRedisTemplate stringRedisTemplate;public static StringRedisTemplate stringRedisTemplateStatic;PostConstruct //在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。public void initStringRedisTemplate(){stringRedisTemplateStaticthis.stringRedisTemplate;}private static final DateTimeFormatter dfDateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss);/** 保存token信息到redis,也可直接在创建token中使用该方法* */public static void redis_SaveTokenInfo(String token,String username){//以username做keyLocalDateTime localDateTimeLocalDateTime.now();stringRedisTemplateStatic.opsForHash().put(username,token,token);stringRedisTemplateStatic.opsForHash().put(username,refreshTime, //有效时间df.format(localDateTime.plus(7*24*60*60*1000, ChronoUnit.MILLIS)));stringRedisTemplateStatic.opsForHash().put(username,expiration, //过期时间 5分钟 300秒df.format(localDateTime.plus(300*1000, ChronoUnit.MILLIS)));stringRedisTemplateStatic.expire(username,7*24*60*60*1000, TimeUnit.SECONDS);}/** 检查redis是否存在token* */public static boolean hasToken(String username){return stringRedisTemplateStatic.opsForHash().getOperations().hasKey(username);}
}
2.3 ResponseUtil
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.apache.ibatis.annotations.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
Data
public class ResponseUtil {public static int OK 200;public static int ERROR 404;public static String SUCCESS操作成功;public static String NO_SUCCESS操作失败请稍候重试。;//返回码(200)private int code;//返回消息private String message;ApiModelProperty(value 返回数据(单条或多条))private MapObject, Object data new HashMapObject, Object();public ResponseUtil(int code, String message) {this.codecode;this.messagemessage;}public ResponseUtil(int code, String message, MapObject, Object data) {this.codecode;this.messagemessage;this.datadata;}//对response写入Object数据public static void reponseOutDiy(HttpServletResponse response,int statusCode , Object result) {ObjectMapper mapper new ObjectMapper();PrintWriter writer null;response.setStatus(statusCode);response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);try {writer response.getWriter();mapper.writeValue(writer, result);writer.flush();} catch (IOException e) {e.printStackTrace();} finally {if (writer ! null) {writer.flush();writer.close();}}}}
2.4 TokenUtil
import com.Lino_white.model.User; //model模块的user
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;public class TokenUtil {public static final String APP_SECRET Lino_white; //随便取你的Token密钥public static final String TOKEN_HEADAuthorization;public static final String TOKEN_PREFIX Bearer ;public static String createToken(User user){String token Jwts.builder().setId(String.valueOf(user.getId())).setSubject(user.getUsername()).setIssuedAt(new Date()) //签发时间.setIssuer(Lino_white) //签发者.setExpiration(new Date(System.currentTimeMillis() 300* 1000)) //过期时间 5分钟 自行设置.signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥.claim(identity, user.getIdentity()) //可添加额外的属性.compact();return token;}//重新生成新的Token异常时间由传入的参数决定public static String createToken(User user,Date expirationTime){SimpleDateFormat f new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);try {expirationTime (Date) f.parse(f.format(expirationTime));} catch (ParseException e) {throw new RuntimeException(e);}String token Jwts.builder().setId(String.valueOf(user.getId())).setSubject(user.getUsername()).setIssuedAt(new Date()) //签发时间.setIssuer(Lino_white) //签发者.setExpiration(expirationTime) //过期时间.signWith(SignatureAlgorithm.HS256, APP_SECRET) //签名算法跟密钥.claim(identity, user.getIdentity()) //可添加额外的属性.compact();return token;}//获得用户名public String getUsernameFromToken(String token){return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getSubject();}/*** 判断token是否存在与有效1*/public boolean checkToken(String token){if (StringUtils.isEmpty(token)) return false;try {Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 判断token是否存在与有效2*/public boolean checkToken(HttpServletRequest request){try {String token request.getHeader(token);return checkToken(token);}catch (Exception e){e.printStackTrace();return false;}}//获得全部属性public Claims parseJwt(String token){Claims claims Jwts.parser().setSigningKey(APP_SECRET) // 设置标识名.parseClaimsJws(token) //解析token.getBody();return claims;}//获得指定属性public String getTokenClaim(String token,String key){Claims body Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();return String.valueOf(body.get(key));}
}
2.5 CorConfig
package com.goyes.common.config;import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 解决跨域* author white*/
Configuration
public class CorConfig implements WebMvcConfigurer {Overridepublic void addCorsMappings(CorsRegistry registry) {System.out.println(开始解决跨域);registry.addMapping(/**).allowedOrigins(*).allowedMethods(*)
// .allowCredentials(true)//是否有验证有就打开.allowedHeaders(*).maxAge(3600);}}3.model模块
3.1 pom dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependencydependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-ribbon/artifactId/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdcom.github.xiaoymin/groupIdartifactIdknife4j-spring-boot-starter/artifactId/dependency
3.2 User
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
AllArgsConstructor
NoArgsConstructor
Data
ApiModel(value 实体用户)
TableName(user)
public class User implements Serializable {ApiModelProperty(用户id)TableId(value id,type IdType.AUTO)private int id;ApiModelProperty(用户名)private String username;ApiModelProperty(密码)private String password;TableField(identity)ApiModelProperty(身份)private String identity;Overridepublic String toString() {return User{ id id , username username \ , password password \ , identity identity \ };}
}3.3 UserFeign
package com.goyes.model.client;import com.goyes.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;FeignClient(name service-user)
public interface UserFeign {GetMapping(/api/user/{username})public User findUserByName(PathVariable(username) String username);}4.service模块 4.1 目录结构 4.2 service_user模块 4.2.1 pom.xml
注意service_user接入了security模块。 !--openfeign 远程接口调用--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependency!--nacos 注册中心--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency!--nacos 配置中心--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId/dependency!--nacos 客户端--dependencygroupIdcom.alibaba.nacos/groupIdartifactIdnacos-client/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdcom.github.xiaoymin/groupIdartifactIdknife4j-spring-boot-starter/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactId/dependencydependencygroupIdorg.springframework/groupIdartifactIdspring-web/artifactIdscopecompile/scope/dependencydependencygroupIdcom.goyes/groupIdartifactIdmodel/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependencydependencygroupIdcom.goyes/groupIdartifactIdcommon/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependency!--加入各service模块swagger文档实现接入--dependencygroupIdcom.goyes/groupIdartifactIdservice_other/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependencydependencygroupIdcom.goyes/groupIdartifactIdservice_course/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependencydependencygroupIdcom.goyes.service_comment/groupIdartifactIdservice_comment/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependency!--接入security模块--dependencygroupIdcom.goyes/groupIdartifactIdspring_security/artifactIdversion1.0-SNAPSHOT/version/dependency4.2.2 application.yml
server:port: 8001
spring:application:name: service-usermain:allow-bean-definition-overriding: trueprofiles:active: devcloud:nacos:config:server-addr: 127.0.0.1:8848group: devdiscovery:cluster-name: WHITE
feign:client:config:default:connect-timeout: 10000read-timeout: 10000 4.2.3 Service_UserApp启动类
SpringBootApplication
EnableSwagger2WebMvc
EnableDiscoveryClient
EnableFeignClients
EnableCaching
public class Service_UserApp
{public static void main( String[] args ){SpringApplication.run(Service_UserApp.class, args);}
} 4.2.4 ApiController控制器
在任意一个控制器中添加如下代码该接口将用于OpenFeign的远程接口调用由security模块中的自定义类MyUserDetailService去进行调用。MyUserDetailService的代码在介绍security模块中会出现 GetMapping(/api/user/{username})public User findUserByName(PathVariable(username) String username){User userByName userService.findUserByName(username);return userByName;}
4.3 其他service模块
对于其他模块相对应跟service_user模块一样进行如下操作即可
1在pom.xml中引入security模块 !--接入security模块--dependencygroupIdcom.goyes/groupIdartifactIdspring_security/artifactIdversion1.0-SNAPSHOT/version/dependency
2在application.xml中添加以下代码
spring:main:allow-bean-definition-overriding: true 防止出现运行异常报错信息对于同一个服务的FeignClient来说配置该属性不会造成覆盖详情可以查看该文章Consider renaming one of the beans
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overridingtrue
5.spring_security模块
5.1 pom
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId/dependencydependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-ribbon/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!--security安全框架--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency!--springboot_redis缓存框架 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactId/dependencydependencygroupIdcom.goyes/groupIdartifactIdmodel/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependencydependencygroupIdcom.goyes/groupIdartifactIdcommon/artifactIdversion1.0-SNAPSHOT/versionscopecompile/scope/dependency
5.2 DiyUserDetailsUserDetails
import com.Lino_white.model.User;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
Data
EqualsAndHashCode(callSuper false)
public class DiyUserDetails extends User implements UserDetails, Serializable {//用户权限列表private CollectionString authorities;Overridepublic Collection? extends GrantedAuthority getAuthorities() {CollectionGrantedAuthority authorities1 new ArrayList();for(String permissionValue : authorities) {if(StringUtils.isEmpty(permissionValue)) continue;SimpleGrantedAuthority authority new SimpleGrantedAuthority(permissionValue);authorities1.add(authority);}return authorities1;}Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic boolean isEnabled() {return true;}
}5.3 WebSecurityConfigWebSecurityConfigurerAdapter
注意前面将对service_user的远程接口定义为/api/user/{username}所以在过滤方面要放行该路径否则security无法调用数据库查询用户信息导致程序报错。
对此可以查看该文章feign.FeignException$Unauthorized
package com.goyes.spring_security.config;import com.goyes.spring_security.filter.TokenAuthenticationFilter;
import com.goyes.spring_security.filter.TokenLoginFilter;
import com.goyes.spring_security.filter.TokenOncePerRequestFilter;
import com.goyes.spring_security.handler.*;
import com.goyes.spring_security.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;Configuration
EnableWebSecurity //开启Security功能
EnableGlobalMethodSecurity(prePostEnabled true) //启动方法级别的权限认证
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate MyUserDetailService myUserDetailService;Bean//配置密码加密器public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//配置哪些请求不拦截//TODO 将需要Feign的方法前缀都用上api得到api/select/user/{user_id}这样的路径不受限制// 由于api路径是由服务模块自己去调用的所以gateway不用做路径请求的处理Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers(/api/**,/doc.html#/**,/swagger-resources);}Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());}//配置安全策略Overrideprotected void configure(HttpSecurity http) throws Exception {System.out.println(读取配置*****************WHITE);http.authorizeRequests().anyRequest().authenticated().and()//该过滤器设置在用户名、密码、权限过滤器之前。这样每次访问接口都会经过此过滤器我们可以获取请求路径并判定当请求路径为/login时进入验证码验证流程。// 使用jwt的Authentication,来解析过来的请求是否有token.addFilterBefore(new TokenOncePerRequestFilter(), UsernamePasswordAuthenticationFilter.class)//登录后,访问没有权限处理类.exceptionHandling().accessDeniedHandler(new NothingAccessDeniedHandler())//匿名访问,没有权限的处理类.authenticationEntryPoint(new LoginAuthenticationEntryPoint()).and().formLogin().successHandler(new LoginInSuccessHandler()).failureHandler(new LoginInFailHandler()).and().logout().logoutSuccessHandler(new LogOutSuccessHandler())// 配置取消session管理.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable();}
}5.4 TokenOncePerRequestFilterOncePerRequestFilter
注意在这里TokenUtil跟RedisUtil对于过期时间的定义不同。
token过期时间为3分钟redis上存储的异常时间为5分钟并且redis上存储的刷新时间为7天
在下面的配置文件中仅仅对token进行分析而已可以根据需要在这里做验证码校验。
token的过期时间在以下代码中是这样做的当token过期时间3分钟到了判断redis上存储的异常时间是否到了5分钟没到5分钟就返回一个新的token给前端前端拿到该token就可以继续访问如果到了5分钟则会停止访问并通知前端 “用户已经过期请重新登录”。
小编有个想法还没做在这里可以重新定义过期时间比如用户每次访问时都进行判断当token的过期时间小于1分钟后就刷新redis的异常时间这样可以使当token要过期时就有新的token出现但这样操作也存在缺点就是要消耗内存资源每次都得去读取token是否临近过期时间了。对于这块可以针对自己的情况去做调整。
package com.goyes.spring_security.filter;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.goyes.common.utils.RedisUtil;
import com.goyes.common.utils.ResponseUtil;
import com.goyes.common.utils.TokenUtil;
import com.goyes.model.User;
import com.goyes.spring_security.model.DiyUserDetails;
import io.github.classgraph.json.JSONUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import jdk.nashorn.internal.parser.JSONParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.json.JsonParser;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import springfox.documentation.spring.web.json.Json;
import sun.security.util.SecurityConstants;
//import sun.security.util.SecurityConstants;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;/*** 在用户名、密码、权限过滤器之前的过滤器* 在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中** TODO 下面过滤器仅做了针对token解析包括token异常、过期、重新颁布等* author white*/
Component
public class TokenOncePerRequestFilter extends OncePerRequestFilter {AutowiredStringRedisTemplate stringRedisTemplate RedisUtil.stringRedisTemplateStatic;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {/** TODO 可在这里判断请求过来的路径是否为login方式为post来在这里进行验证码有效验证* 验证成功则直接chain(request,response)继续走过滤* */String requestURI request.getRequestURI();System.out.println(开始请求请求路径requestURI 请求方式request.getMethod());User user null;SimpleDateFormat f new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);String authHeader request.getHeader(TokenUtil.TOKEN_HEAD);//没有token不用理if (authHeader ! null authHeader.startsWith(TokenUtil.TOKEN_PREFIX)) {final String authToken authHeader.replace(TokenUtil.TOKEN_PREFIX, );//这里的authToken可能时间已过需要重新创建一个token//先对比redis中的过期时间redis的过期时间随着用户的操作而更新token可能没有及时更新//判断是否一样一样的话就是token失效了跳转重新登录// 不一样就是redis过期时间更新了生成新的token返回给前端String username null;Claims claims;try {claims new TokenUtil().parseJwt(authToken);username claims.getSubject();} catch (ExpiredJwtException e) {//token过期claims e.getClaims();username claims.getSubject();user JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, user)), User.class);if (user null) {chain.doFilter(request, response);return;} else {if (RedisUtil.hasToken(username)) {Object expiration stringRedisTemplate.opsForHash().get(username, expiration);Object tokenExpirationTime f.format(claims.getExpiration());Date expirationDate_redisTime null, expirationDate_tokenTime null, nowTime;try {expirationDate_redisTime (Date) f.parseObject(String.valueOf(expiration));expirationDate_tokenTime (Date) f.parseObject(String.valueOf(tokenExpirationTime));nowTime (Date) f.parseObject(f.format(new Date()));} catch (ParseException ex) {throw new RuntimeException(ex);}System.out.println(*********Token过期Start***********);System.out.println(token浏览器过期时间 tokenExpirationTime);System.out.println(redis过期时间 expiration);///** redistoken || tokenredis || redis now 则token失效跳转登录* tokenredis* */if (expirationDate_redisTime.getTime() expirationDate_tokenTime.getTime() ||expirationDate_tokenTime.getTime() expirationDate_redisTime.getTime() ||expirationDate_redisTime.getTime() nowTime.getTime()) {//时间相同跳转登录ResponseUtil.reponseOutDiy(response, 401, 用户已过期请重新登录);System.out.println(*********Token过期End失效***********);return;} else {//时间不同生成新token 需要用户id身份用户名//response存入token 返回Object expiration_redisTime stringRedisTemplate.opsForHash().get(username, expiration);Date date;try {date (Date) f.parseObject(String.valueOf(expiration_redisTime));} catch (ParseException ex) {throw new RuntimeException(ex);}//通过数据库查询数据创建tokenSystem.out.println(这里之前开始的时间 date);String token TokenUtil.createToken(user, date);System.out.println(—————————————————start—————————————————————);System.out.println(token token);RedisUtil.redis_SaveTokenInfo(user, token);response.setHeader(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX token);request.setAttribute(TokenUtil.TOKEN_HEAD, TokenUtil.TOKEN_PREFIX token);Date expiration1 new TokenUtil().parseJwt(token).getExpiration();System.out.println(重新更新token后过期时间 expiration1);System.out.println(—————————————————End—————————————————————);ResponseUtil.reponseOutDiy(response, 200, token);System.out.println(*********Token过期End新Token***********);return;}} else {//TODO 新增如果redis没有username说明未登录throw new RuntimeException(未登录);}}}//避免每次请求都请求数据库查询用户信息从缓存中查询user JSONObject.parseObject(String.valueOf(stringRedisTemplate.opsForHash().get(username, user)), User.class);if (username ! null SecurityContextHolder.getContext().getAuthentication() null) {if (user ! null) {UsernamePasswordAuthenticationToken authentication // TODO 未修改 这里的权限先空着new UsernamePasswordAuthenticationToken(user, user.getPassword(), null);authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}}System.out.println(走过滤——————————————————————————);chain.doFilter(request, response);}
}5.5 LoginAuthenticationEntryPointAuthenticationEntryPoint
import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import jdk.nashorn.internal.parser.Token;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*** 匿名未登录的时候访问,需要登录的资源的调用类* author Lino_white*/
Component
public class LoginAuthenticationEntryPoint implements AuthenticationEntryPoint {Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {String token httpServletRequest.getHeader(TokenUtil.TOKEN_HEAD);System.out.println(当前未登录无法访问 ::token);if (token!null token.contains(TokenUtil.TOKEN_PREFIX)) {tokentoken.replace(TokenUtil.TOKEN_PREFIX,);String usernameFromToken new TokenUtil().getUsernameFromToken(token);System.out.println(用户名usernameFromToken);}ResponseUtil.reponseOutDiy(httpServletResponse,401,当前未登录无法访问);}
}
5.6 LoginInFailHandlerAuthenticationFailureHandler
import com.Lino_white.common.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*** 登录账号密码错误等情况下,会调用的处理类* author Lino_white*/
Component
public class LoginInFailHandler implements AuthenticationFailureHandler {Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {System.out.println(认证失败————————————);ResponseUtil.reponseOutDiy(httpServletResponse,401,登录失败请重试);}
}5.7 LoginInSuccessHandlerAuthenticationSuccessHandler
package com.goyes.spring_security.handler;import com.fasterxml.jackson.databind.ObjectMapper;
import com.goyes.common.utils.RedisUtil;
import com.goyes.common.utils.ResponseUtil;
import com.goyes.common.utils.TokenUtil;
import com.goyes.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/*** LoginInSuccessHandler.java的作用* 登录成功处理类,登录成功后会调用里面的方法* author white文* time: 2023/5/18 16:02*/
Slf4j
Component
public class LoginInSuccessHandler implements AuthenticationSuccessHandler {/*** 用户通过TokenLoginFilterUsernamePasswordAuthenticationFilter后* 验证成功到这里进行* 1.获取当前用户* 2.token创建* 3.并将其存入redis并返回*/Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {log.info(登录成功开始初始化token并缓存在redis);User user (User) authentication.getPrincipal();String token TokenUtil.createToken(user);//redis缓存tokenRedisUtil.redis_SaveTokenInfo(user,token);//写入responseresponse.setHeader(token, TokenUtil.TOKEN_PREFIXtoken);try {//登录成功返回json格式进行提示response.setContentType(application/json;charsetutf-8);response.setStatus(HttpServletResponse.SC_OK);PrintWriter outresponse.getWriter();MapString,Object mapnew HashMapString,Object(4);map.put(code,HttpServletResponse.SC_OK);map.put(message,这里全部都是自定义的登录成功);map.put(token,token);out.write(new ObjectMapper().writeValueAsString(map));out.flush();out.close();}catch (Exception e){e.printStackTrace();}}}5.8 LogOutSuccessHandlerLogoutSuccessHandler
import com.Lino_white.common.RedisUtil;
import com.Lino_white.common.ResponseUtil;
import com.Lino_white.common.TokenUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;Component
public class LogOutSuccessHandler implements LogoutSuccessHandler {private StringRedisTemplate stringRedisTemplate RedisUtil.stringRedisTemplateStatic;Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//用户退出登录System.out.println(LogoutSuccessHandler退出);String tokenrequest.getHeader(token);if (tokennull) tokenrequest.getHeader(TokenUtil.TOKEN_HEAD);tokentoken.replace(TokenUtil.TOKEN_PREFIX,);String username new TokenUtil().getUsernameFromToken(token);Authentication au SecurityContextHolder.getContext().getAuthentication();if (au!null) new SecurityContextLogoutHandler().logout(request,response,au);Boolean delete stringRedisTemplate.delete(username);if (delete) ResponseUtil.reponseOutDiy(response,200,用户已成功退出);}
}
5.9 NothingAccessDeniedHandlerAccessDeniedHandler
import com.Lino_white.common.ResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*** 没有权限,被拒绝访问时的调用类* author Lino_white*/
Component
public class NothingAccessDeniedHandler implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {System.out.println(没有权限);ResponseUtil.reponseOutDiy(httpServletResponse,403,当前您没有该权限);}
}
5.10 MyUserDetailServiceUserDetailsService
注意在这里调用了model模块中的UserFeign文件实现读取service_user模块中的findUserByName方法。
package com.goyes.spring_security.service;import com.goyes.model.User;
import com.goyes.model.client.UserFeign;
import com.goyes.spring_security.model.DiyUserDetails;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** 从数据库读取用户信息用户名密码身份进行身份认证*/
Service
public class MyUserDetailService implements UserDetailsService{Autowiredprivate UserFeign userFeign;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {System.out.println(********开始loadUserByUsername********);User user userFeign.findUserByName(username);System.out.println(浏览器的usernameusername);if (usernull) throw new UsernameNotFoundException(username);System.out.println(数据库的usernameuser.getUsername());//根据当前用户名查询用户权限ListString authoritiesnew ArrayList();authorities.add(ROLE_user.getIdentity());DiyUserDetails detailsnew DiyUserDetails();BeanUtils.copyProperties(user,details);details.setAuthorities(authorities);//如果数据库密码无加密用下列//details.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));System.out.println(********结束loadUserByUsername********);return details;}
}6.gateway模块
6.1 pom dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactId/dependency
6.2 application.yml
server:port: 10000
spring:application:name: gatewaycloud:gateway:routes:- id: useruri: http://localhost:8001predicates:- Path/user/**,/admin/**,/api/user/**,/login,/logout- id: courseuri: http://localhost:8002predicates:- Path/course/**7.测试 这里是使用postman工具进行测试的在这里之前已经在数据库有用户名跟密码都为111的数据并且密码已是加密形式。 1.首次访问/admin/findAllgateway会调用到8001端口下的user模块如下图 首次访问/course/findAllgateway会调用到8002端口下的course模块如下图 2.POST访问/login并且提供相关参数数据库存在用户111和加密过的密码111得到token如下图 这时redis数据库就有了用户名为 user的数据 3.复制刚才返回给前端的token在Authorization的Type中选择Bearer Token粘贴上刚才的Token点击Send发送 5.再次请求8002端口下的source模块 再次请求8001端口下的user模块 6.当token过期后redis中的异常时间还没到则会返回给前端一个新的token拿着新token继续请求即可。 7.当token过期并且redis的异常时间也过了之后用户就需要重新登录。 8.退出则为/logout 。 同时redis数据库中用户名为user的key值也被删除掉。 至此结束祝大家520快乐