可能有些人会觉得这篇似曾相识,没错,这篇是由原文章进行二次开发的。
前阵子有些事情,但最近看到评论区说原文章最后实现的是单模块的验证,由于过去太久也懒得验证,所以重新写了一个完整的可以跑得动的一个。
OK,回到正题,以下是真正对应的微服务多模块的一个方法,使用到的技术有:基于微服务的Springboot+Security+Redis+Gateway+OpenFeign+Nacos+JWT。
对使用到的微服务技术进行在项目中的说明:
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 DiyUserDetails(UserDetails)
5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)
5.4 TokenOncePerRequestFilter(OncePerRequestFilter)
5.5 LoginAuthenticationEntryPoint(AuthenticationEntryPoint)
5.6 LoginInFailHandler(AuthenticationFailureHandler)
5.7 LoginInSuccessHandler(AuthenticationSuccessHandler)
5.8 LogOutSuccessHandler(LogoutSuccessHandler)
5.9 NothingAccessDeniedHandler(AccessDeniedHandler)
5.10 MyUserDetailService(UserDetailsService)
6.gateway模块
6.1 pom
6.2 application.yml
7.测试
1.项目结构
涉及的模块有
(1)common(Redis配置文件、Redis工具、Token工具、返回给前端信息的工具;即如下文件RedisConfig、RedisUtil、TokenUtil、ResponseUtil);
(2)gateway;
(3)model(实体类,Feign的客户端);
(4)service(用户模块、课程模块);
(5)spring_security(security的过滤器跟配置文件)。
下面小编将全部一一介绍并且源码展示出来。
2.Common模块
pom.xml
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 {
@Resource
RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate
RedisTemplate
redisTemplate.setConnectionFactory(redisConnectionFactory);
//Json序列化配置
//1、String的序列化
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
//2、json解析任意的对象(Object),变成json序列化
Jackson2JsonRedisSerializer
ObjectMapper mapper=new ObjectMapper(); //用ObjectMapper进行转义
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//该方法是指定序列化输入的类型,就是将数据库里的数据按照一定类型存储到redis缓存中。
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(serializer);
// hash的value序列化方式采用jackson
redisTemplate.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 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static StringRedisTemplate stringRedisTemplateStatic;
@PostConstruct //在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
public void initStringRedisTemplate(){
stringRedisTemplateStatic=this.stringRedisTemplate;
}
private static final DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/*
* 保存token信息到redis,也可直接在创建token中使用该方法
* */
public static void redis_SaveTokenInfo(String token,String username){
//以username做key
LocalDateTime localDateTime=LocalDateTime.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 Map
public ResponseUtil(int code, String message) {
this.code=code;
this.message=message;
}
public ResponseUtil(int code, String message, Map
this.code=code;
this.message=message;
this.data=data;
}
//对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_HEAD="Authorization";
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 {
@Override
public void addCorsMappings(CorsRegistry registry) {
System.out.println("开始解决跨域");
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
// .allowCredentials(true)//是否有验证,有就打开
.allowedHeaders("*")
.maxAge(3600);
}
}
3.model模块
3.1 pom
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;
@Override
public 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模块。
4.2.2 application.yml
server:
port: 8001
spring:
application:
name: service-user
main:
allow-bean-definition-overriding: true
profiles:
active: dev
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
group: dev
discovery:
cluster-name: WHITE
feign:
client:
config:
default:
connect-timeout: 10000
read-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模块
(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-overriding=true
5.spring_security模块
5.1 pom
5.2 DiyUserDetails(UserDetails)
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 Collection
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection
for(String permissionValue : authorities) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities1.add(authority);
}
return authorities1;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5.3 WebSecurityConfig(WebSecurityConfigurerAdapter)
注意:前面将对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 {
@Autowired
private MyUserDetailService myUserDetailService;
@Bean
//配置密码加密器
public PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
//配置哪些请求不拦截
//TODO 将需要Feign的方法前缀都用上api,得到api/select/user/{user_id}这样的路径不受限制
// 由于api路径是由服务模块自己去调用的,所以gateway不用做路径请求的处理
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**","/doc.html#/**","/swagger-resources");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
//配置安全策略
@Override
protected 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 TokenOncePerRequestFilter(OncePerRequestFilter)
注意:在这里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 {
@Autowired
StringRedisTemplate stringRedisTemplate = RedisUtil.stringRedisTemplateStatic;
@Override
protected 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);
//
/*
* redis * token * */ 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); } //通过数据库查询数据,创建token System.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 LoginAuthenticationEntryPoint(AuthenticationEntryPoint) 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 { @Override public 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)) { token=token.replace(TokenUtil.TOKEN_PREFIX,""); String usernameFromToken = new TokenUtil().getUsernameFromToken(token); System.out.println("用户名:"+usernameFromToken); } ResponseUtil.reponseOutDiy(httpServletResponse,401,"当前未登录,无法访问"); } } 5.6 LoginInFailHandler(AuthenticationFailureHandler) 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 { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { System.out.println("认证失败————————————"); ResponseUtil.reponseOutDiy(httpServletResponse,401,"登录失败,请重试"); } } 5.7 LoginInSuccessHandler(AuthenticationSuccessHandler) 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 { /** * 用户通过TokenLoginFilter(UsernamePasswordAuthenticationFilter)后, * 验证成功到这里进行 * 1.获取当前用户 * 2.token创建 * 3.并将其存入redis并返回 */ @Override public 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缓存token RedisUtil.redis_SaveTokenInfo(user,token); //写入response response.setHeader("token", TokenUtil.TOKEN_PREFIX+token); try { //登录成功,返回json格式进行提示 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter out=response.getWriter(); Map 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 LogOutSuccessHandler(LogoutSuccessHandler) 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; @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //用户退出登录 System.out.println("LogoutSuccessHandler退出"); String token=request.getHeader("token"); if (token==null) token=request.getHeader(TokenUtil.TOKEN_HEAD); token=token.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 NothingAccessDeniedHandler(AccessDeniedHandler) 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 { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { System.out.println("没有权限"); ResponseUtil.reponseOutDiy(httpServletResponse,403,"当前您没有该权限"); } } 5.10 MyUserDetailService(UserDetailsService) 注意:在这里调用了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{ @Autowired private UserFeign userFeign; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("********开始loadUserByUsername********"); User user = userFeign.findUserByName(username); System.out.println("浏览器的username:"+username); if (user==null) throw new UsernameNotFoundException(username); System.out.println("数据库的username:"+user.getUsername()); //根据当前用户名查询用户权限 List authorities.add("ROLE_"+user.getIdentity()); DiyUserDetails details=new 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 6.2 application.yml server: port: 10000 spring: application: name: gateway cloud: gateway: routes: - id: user uri: http://localhost:8001 predicates: - Path=/user/**,/admin/**,/api/user/**,/login,/logout - id: course uri: http://localhost:8002 predicates: - Path=/course/** 7.测试 这里是使用postman工具进行测试的,在这里之前已经在数据库有用户名跟密码都为111的数据,并且密码已是加密形式。 1.首次访问/admin/findAll,gateway会调用到8001端口下的user模块,如下图 首次访问/course/findAll,gateway会调用到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快乐!!! 推荐链接
发表评论