SpringBoot结合JWT+Shiro+Redis实现token无状态登录授权
一、引言
在微服务中我们一般采用的是无状态登录,而传统的session方式,在前后端分离的微服务架构下,如继续使用则必将要解决跨域sessionId问题、集群session共享问题等等。这显然是费力不讨好的,而整合shiro,却很不恰巧的与我们的期望有所违背,原因:
(1)shiro默认的拦截跳转都是跳转url页面,而前后端分离后,后端并无权干涉页面跳转。
(2)shiro默认使用的登录拦截校验机制恰恰就是使用的session。
这当然不是我们想要的,因此如需使用shiro,我们就需要对其进行改造,那么要如何改造呢?我们可以在整合shiro的基础上自定义登录校验,继续整合JWT,或者oauth2.0等,使其成为支持服务端无状态登录,即token登录。
二、相关说明
2.1. Shiro + JWT实现无状态鉴权机制
1. 首先post用户名与密码到login进行登入,如果成功在请求头Header返回一个加密的Authorization,失败的话直接返回未登录,以后访问都带上这个Authorization即可。
2. 鉴权流程主要是要重写shiro的入口过滤器BasicHttpAuthenticationFilter,在此基础上进行拦截、token验证授权等操作
2.2. 关于AccessToken及RefreshToken概念说明
1. AccessToken:用于接口传输过程中的用户授权标识,客户端每次请求都需携带,出于安全考虑通常有效时长较短。
2. RefreshToken:与AccessToken为共生关系,一般用于刷新AccessToken,保存于服务端,客户端不可见,有效时长较长。
2.3. 关于Redis中保存RefreshToken信息(做到JWT的可控性)
1. 登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证。
2. Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户
2.4. 关于根据RefreshToken自动刷新AccessToken
1. 本身AccessToken的过期时间为5分钟,RefreshToken过期时间为30分钟,当登录后时间过了5分钟之后,当前AccessToken便会过期失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过期,开始判断是否要进行AccessToken刷新,首先redis查询RefreshToken是否存在,以及时间戳和过期AccessToken所携带的时间戳是否一致,如果存在且一致就进行AccessToken刷新。
2. 刷新后新的AccessToken过期时间依旧为5分钟,时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期,最终将刷新的AccessToken存放在Response的Header中的Authorization字段返回。
3. 同时前端进行获取替换,下次用新的AccessToken进行访问即可。
三、项目准备配置
项目结构:

pom.xml
该项目要用到的组件有java-jwt、json、shiro-spring、spring-boot-starter-data-redis等。
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
com.ljnt
blog
0.0.1-SNAPSHOT
blog
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
com.auth0
java-jwt
3.9.0
org.json
json
20190722
org.apache.shiro
shiro-spring
1.4.0
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-maven-plugin
application.yml
主要是配置redis,需要用到模板、数据库、日志的,请看注释
server:
port: 8181
#spring:
# thymeleaf:
# mode: HTML5
#
# datasource:
# driver-class-name: com.mysql.jdbc.Driver
# url: jdbc:mysql://localhost:3306/blog?useSSL=false&characterEncoding=utf-8
# username: root
# password:
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: -1
max-wait: 3000ms
timeout: 3000ms
#logging:
# level:
# root: info
# com.ljnt: debug
# file: log/imcoding.log
四、实现颁发token
实现颁发token需要用到JWT和Redis,所以我们需要配置Redis和实现工具类。
4.1. 配置Redis:RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
@Bean
public HashOperations hashOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations listOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations setOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations zSetOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForZSet();
}
}
4.2. 编写工具类
RedisUtil,这里主要用到redisTemplate的一些方法,代码没有全部给出来,可以根据redisTemplate方法去编写或者看我的源码。。
@Component
public class RedisUtil {
@Autowired
private static RedisTemplate redisTemplate;
public RedisUtil(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static boolean expire(String key,long time){
try {
if(time>0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
public static boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
//=========代码太长,省略代码
}
TokenUtil,主要实现token的签发、验证和数据解析。
public class TokenUtil {
//这里的token属性配置最好写在配置文件中,这里为了方面直接写成静态属性
public static final long EXPIRE_TIME= 5*60*1000;//token到期时间5分钟,毫秒为单位
public static final long REFRESH_EXPIRE_TIME=30*60;//RefreshToken到期时间为30分钟,秒为单位
private static final String TOKEN_SECRET="ljdyaishijin**3nkjnj??"; //密钥盐
public static String sign(String account,Long currentTime){
String token=null;
try {
Date expireAt=new Date(currentTime+EXPIRE_TIME);
token = JWT.create()
.withIssuer("auth0")//发行人
.withClaim("account",account)//存放数据
.withClaim("currentTime",currentTime)
.withExpiresAt(expireAt)//过期时间
.sign(Algorithm.HMAC256(TOKEN_SECRET));
} catch (IllegalArgumentException|JWTCreationException je) {
}
return token;
}
public static Boolean verify(String token) throws Exception{
JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();//创建token验证器
DecodedJWT decodedJWT=jwtVerifier.verify(token);
System.out.println("认证通过:");
System.out.println("account: " + decodedJWT.getClaim("account").asString());
System.out.println("过期时间: " + decodedJWT.getExpiresAt());
return true;
}
public static String getAccount(String token){
try{
DecodedJWT decodedJWT=JWT.decode(token);
return decodedJWT.getClaim("account").asString();
}catch (JWTCreationException e){
return null;
}
}
public static Long getCurrentTime(String token){
try{
DecodedJWT decodedJWT=JWT.decode(token);
return decodedJWT.getClaim("currentTime").asLong();
}catch (JWTCreationException e){
return null;
}
}
}
4.3. 编写登录接口:LoginController
登录成功颁发token,生成RefreshToken保存在redis,返回在Header的Authorization中。
@Controller
public class LoginController {
@Autowired
RedisUtil redisUtil;
@PostMapping("/login")
@ResponseBody
public Result login(String username, String password, HttpServletResponse response) throws JsonProcessingException {
User user=new User();
user.setUsername(username);
user.setPassword(password);
//去数据库拿密码验证用户名密码,这里直接验证
if(username.equals("admin")){
if (!password.equals("admin")){
return new Result(400,"密码错误");
}
}else if (username.equals("user")){
if (!password.equals("user")){
return new Result(400版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/ljlj8888/article/details/104826421
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。