本文的代码例子是项目中的代码,单纯理解基础可以不看代码
目录
1、创建Token的封装类oAuth2Token 。(filter中常见令牌使用)
一)JWT与Shiro基础:
1、单点登录:
了解JWT之前,我们先了解一下JWT的最多应用场景----单点登录。
图中有四个系统,sso只有登录的功能,1系统处理订单,2系统处理购物车,3系统处理用于数据的。如果没有单点登录,管理者就需要登录4次系统输入4次用户名和密码,这就导致我们要输入很多次用户名和密码。于是出现了单点登录,只需要在sso登录一次,然后所有与其绑定的信任系统都不用再次登录就可以使用。
再举一个简单的例子,淘宝和天猫是两个相互信任的系统,你会发现当你登录了天猫或者淘宝之后另一个系统就不用登录即可使用。
2、JWT
1、基本理解:说白了就是为了实现单点登录(一次登录一段时间之内该应用及其信任应用都不用再次登录)
2、实现原理:第一次登录之后服务器A会根据内部的加密算法给客户端返回一串字符串叫做令牌,用于验证,当下次使用时,客户端将令牌发给服务器B,B会根据发来的令牌以及服务器内部令牌解密算法进行令牌解密,成功就允许用户使用相关功能,失败就会抛出异常拒绝访问。
3、每个jsw令牌都存在一定的时间(自己设定),超出这个时间令牌就会失效,需要重新登录
二)SpringBoot整合JWT与shiro。
一)依赖导入及令牌的创建
1、导入JWT与shiro需要的MAVEN依赖。
<!--授权与令牌-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
yml文件内容
emos:
jwt:
secret: abc123456
expire: 5 服务端令牌保存时间
cache-expire: 10 缓存中保存时间(设置为服务端的两倍)
2、创建JWTUtil类用于创建、解密和验证令牌。
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component//令牌注入到容器
public class JwtUtil {
//注解将某个值注给下面的变量
@Value("${emos.jwt.secret}")
private String secret;//生成算法用
@Value("${emos.jwt.expire}")
private int expire;//保存的时间
//1、通过唯一的openID生成令牌
public String createToken(int userId){
//1、确定保存时间
/**第一个参数是当前时间,第二个参数是单位,第三个参数是保存时间*/
Date date = DateUtil.offset(new Date(),
DateField.DAY_OF_YEAR, 5);
//2、根据密钥创建加密算法对象,使用JWT包下的
Algorithm algorithm=Algorithm.HMAC256(secret);
//3、创建加密内部类的对象
JWTCreator.Builder builder= JWT.create();
//4、内部类对象中的链式调用创建令牌
String token= builder.withClaim("userId",userId) //用户信息
.withExpiresAt(date) //确定保存时间
.sign(algorithm); //使用的算法
//5、返回令牌数据
return token;
}
//2、通过令牌来解密出用户的openId
public int getUserId(String token){
//1、通过令牌创建解码对象
DecodedJWT jwt = JWT.decode(token);
//2、对象通过设置令牌withClaim的name属性获得Id,并且强转为int类型
Integer userId = jwt.getClaim("userId").asInt();
//3、返回唯一标识id
return userId;
}
//3、验证令牌的有效性,如果出现问题就会自动抛出异常,RunTime异常
public void verifierToken(String token){
//1、传入密钥确定使用的算法
Algorithm algorithm = Algorithm.HMAC256(secret);
//2、用JWT的方法根据算法生成验证对象
/**
* 1、JWT.require(algorithm)进行解密
* 2、.build来创建验证对象
* */
JWTVerifier verifier = JWT.require(algorithm).build();
//3、验证对象调用verify方法进行验证
verifier.verify(token);
}
}
二)JWT与shiro执行大致流程
JWT与shiro只有对接起来,shiro才能才会拦截HTTP请求来验证Token是否有效,而刚刚生成的Token令牌是字符串类型,字符串类型的Token令牌不能直接供shiro框架使用,而需要封装为一个类(在此命名为AuthenticationToken)提供给shiro框架。
整体的类以及执行过程大致如下:
1、将生成的令牌封装为一个类(oAuth2Token)并且实现AuthenticationToken接口提供给shiro框架。
2、创建AuthorizingRealm实现 接口进行Token的验证
3、创建AuthenticatingFilter用于拦截那些没有权限的HTTP请求
4、将之前写的类添加到shiro框架中。
三)代码实现:
1、创建Token的封装类oAuth2Token 。(filter中常见令牌使用)
1、创建类(oAuth2Token)实现AuthenticationToken接口。
2、实现接口里面的两个方法,并且提供构造方法以及token属性。
import org.apache.shiro.authc.AuthenticationToken;
//Shiro对象不能直接接收一个字符串令牌Token,
// 必须接收一个实现了AuthenticationToken对象
//实现过AuthenticationToken的类就是Shiro可以接收的类
//所以在此将令牌封装为一个对象,以便后期给Shiro进行验证
public class oAuth2Token implements AuthenticationToken {
private String token;
public oAuth2Token(String token){
this.token=token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2、创建shiro框架的认证类OAuth2Realm。
1、继承AuthorizingRealm类。
2、打上@Component注解加入容器中。
3、实现继承的抽象类里的三个方法。
//实现shiro的认证与授权
import com.example.emos.db.pojo.TbUser;
import com.example.emos.service.userService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* 1、继承AuthorizingRealm类,并且重写里面的方法,以及supports方法
* */
@Component
public class OAuth2Realm extends AuthorizingRealm {
//创建、验证、获取userId
@Autowired
private JwtUtil jwtUtil;
@Autowired
private userService userService;
//验证令牌对象是否为自己定义的令牌封装类型。参数是接口类型
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof oAuth2Token;
}
//验证登录时使用,返回值为认证对象类型
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken Token) throws AuthenticationException {
//通过Token.getPrincipal()获取令牌
String accessToken = (String) Token.getPrincipal();
int userId = jwtUtil.getUserId(accessToken);//得到用户的主键值
TbUser user=userService.searchById(userId);//通过主键值得到用户信息
if (user==null){
throw new LockedAccountException("用户不存在");
}
//创建认证对象SimpleAuthenticationInfo
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user,accessToken,getName());
return info;
}
//授权方法(验证权限时用)返回为授权对象类型
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
TbUser user=(TbUser)principalCollection.getPrimaryPrincipal();
int userId=user.getId();
Set<String> permsSet=userService.searchUserPermission(userId);
//1、创建授权对象SimpleAuthorizationInfo
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
//2、查询用户权限
info.setStringPermissions(permsSet);
//3、将权限添加到info对象中
return info;
}
}
3、令牌刷新机制:
令牌刷新机制:只要当令牌过期用户就会失去对某些API的访问,但是用户在这一段时间一直在使用这个应用,所以我们希望只要用户在使用这个APP令牌就会自动的续期,于是出现了令牌刷新机制。
主流的令牌刷新机制有两种:一种是双令牌。二是缓存令牌。本文采用第二种。
1、双令牌:即向用户提供两个令牌,一个长时间的令牌一个短时间的,只有两者都过期时才会让用户重新登录。两个令牌是不同的。
2、缓存令牌:即给用户一个令牌,服务端也保存一个令牌。且缓存令牌的时间是客户端令牌时间的一倍。则分为两种情况:1是客户端令牌过期,缓存令牌也过期,此时需要重新登录。2是客户端令牌过期缓存令牌没有过期,此时后台需要生成新的令牌给客户端,并且服务端将新的令牌也缓存到缓存数据中。缓存和客户端的令牌是相同的。
大致实现过程:1、由OAuth2Filter进行判断,发现客户端令牌过期服务端令牌没有过期于是调用JwtUtil类生成新的令牌。2、将新生成的令牌暂存在ThreadLocalToken类(避免多线程访问一个变量造成线程不安全问题)中后期返回给客户端。3、然后再将该新令牌传给客户端的缓存Redies中。此时2和3中都保存有新令牌。4、OAuth2Filter类放行该用户的API请求,于是进入Controller类。5、通过自定义的TokenAspect类拦截掉该返回的请求。6、从ThreadLocalToken中提取令牌字符串,并且绑定在返回对象R中。7、将R对象返回给客户端,客户端通过将R对象解析为JSON对象来提取内部的令牌信息。
1、客户端实现令牌更新:
2、实现ThreadLocalToken类:实现临时保存令牌的作用。
ThreadLocal:多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
import org.springframework.stereotype.Component;
@Component//方便以后使用直接注入
//作为媒介类,将新令牌放进去,以及取出这个新令牌
public class ThreadLocalToken {
//解决多线程变量不安全问题
/*从名字我们就可以看到ThreadLocal 叫做本地线程变量,
意思是说,ThreadLocal 中填充的的是当前线程的变量,
该变量对其他线程而言是封闭且隔离的,ThreadLocal
为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。*/
private ThreadLocal<String> local=new ThreadLocal<>();
//设置令牌
public void setToken(String token){
local.set(token);
}
//取出令牌
public String getToken(){
return local.get();
}
//将绑定的数据清除
public void clear(){
local.remove();
}
}
4、创建shiro框架的拦截类OAuth2Filter。
1、filter类的作用:三种判断
1、有权限,通过,则可以执行相对应的功能。(options请求:小程序向服务端发送请求会分为两次第一次是options请求,就是看看服务器能否接收这种类型的数据,第二次才是真正的post类型)
2、客户端令牌过期,服务端没过期,生成新的令牌返回客户端,同时更新服务端缓存中令牌。
3、客户端和缓存中都过期了,重新登陆。
2、OAuth2Filter类书写
1、继承AuthenticatingFilter类,覆盖createToken、isAccessAllowed、onAccessDenied、onLoginFailure、doFilterInternal方法
2、加上@Component @Scope("prototype")注解
package com.example.emos.config.shiro;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Component
@Scope("prototype")//多例模型,默认spring容器会只创建一个对象,全程每次引用都会使用这一个对象
//加了这个注解之后会每次注入都会生成一个新的对象
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired//引入媒介类,获取里面的令牌
private ThreadLocalToken threadLocalToken;
//缓存中存取的时间
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Autowired//使用和验证令牌
private JwtUtil jwtUtil;
@Autowired//用于操作redis对象
private RedisTemplate redisTemplate;
//从请求头或请求体中获取token字符串
private String getRequestToken(HttpServletRequest request){
String token=request.getHeader("token");//获取header中名为token的数据
//StrUtil.isBlank()判断是否为null或者空字符串
//如果token不在请求头中就尝试在请求体中获取数据
if(StrUtil.isBlank(token)){
//getParameter从请求体中获得数据
token=request.getParameter("token");
}
return token;
}
@Override//生成令牌字符串并且封装为令牌类型给shiro使用
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) throws Exception {
//getRequestToken上面定义的方法获取token字符串,参数必须是HttpServletRequest类型
String token=getRequestToken((HttpServletRequest) request);
if(StrUtil.isBlank(token)){
return null;
}
return new oAuth2Token(token);//返回令牌对象类型
}
//判断哪种请求应该(post/get)被框架处理,那种不应该被shiro框架(options请求)处理
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req= (HttpServletRequest) request;
//if中是固定格式,就是为了判断是不是options请求
if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
//options请求直接放行返回true
return true;
}
//非options请求需要处理
return false;
}
//上面的如果判断出要被shiro处理,则进入该方法
/**功能:
* 1、获取token字符串判断是否过期。
* 2、如果过期则生成令牌字符串保存在缓存和媒介类中
* */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req= (HttpServletRequest) request;
HttpServletResponse resp= (HttpServletResponse) response;
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
//清除媒介类中原有的数据
threadLocalToken.clear();
//获取令牌字符串
String token=getRequestToken(req);
if(StrUtil.isBlank(token)){
//401错误情况
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
try{
//逻辑:如果令牌有误就会抛出异常
jwtUtil.verifierToken(token);
}catch (TokenExpiredException e){//令牌过期异常
//查看缓存中令牌有没有过期
if(redisTemplate.hasKey(token)){//缓存中没过期,客户端的过期了
//清空缓存中的令牌
redisTemplate.delete(token);
//通过老令牌得到用户的userId
int userId=jwtUtil.getUserId(token);
//生成新的令牌
token=jwtUtil.createToken(userId);
redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
threadLocalToken.setToken(token);
}
else{
//设置返回的状态码
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
//设置返回的消息
resp.getWriter().print("令牌已过期");
return false;
}
}catch (JWTDecodeException e){//令牌是一个错误的令牌
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
//executeLogin调用realm类判断认证和授权的成功与否
boolean bool=executeLogin(request,response);
return bool;//false认证或授权失败都返回false
}
//shiro令牌没有认证或者认证失败
//给客户端返回个错误消息
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req= (HttpServletRequest) request;
HttpServletResponse resp= (HttpServletResponse) response;
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try{
resp.getWriter().print(e.getMessage());
}catch (Exception exception){
}
return false;
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request,response, chain);
}
}
5、创建shiro框架的拦截类ShiroConfig。
将realm类和filter类添加到shiro框架,因为刚刚写的所有类都是自己创建的javaBean而不是能被框架识别的对应类。
package com.example.emos.config.shiro;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
//自己创建的realm以及filter类只是单纯的javaBean
//不能供springBoot使用,于是这个类将其配置到框架之中
@Configuration
public class ShiroConfig {
//将realm类添加到框架
@Bean("securityManager")//把该类的返回值注入到容器,名称为参数
public SecurityManager securityManager(OAuth2Realm realm){
//1、构建SecurityManager子类用于封装realm类
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//2、添加固定两部
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
//将filter添加到框架
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager
,OAuth2Filter filter){
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//filter对象不能直接供框架使用,要将其封装为map对象
Map<String , Filter> map=new HashMap<>();
map.put("oauth2",filter);
shiroFilter.setFilters(map);//将filter对象放进去
//设置拦截路径
Map<String,String> filterMap=new LinkedHashMap<>();
//路径为anon标识不用被拦截,oauth2表示拦截,是上面定义的
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/test/**", "anon");
filterMap.put("/meeting/recieveNotify", "anon");
filterMap.put("/**", "oauth2");//除了上面的全部都是要拦截的
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
//管理shiro对象的生命周期,直接返回即可
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
//AOP切面类,在web方法执行前执行,验证是否有权限
//filter负责拦截对应的接口,AuthorizationAttributeSourceAdvisor用于验证有无权限访问
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
6、创建拦截切片类TokenAspect。
(在数据返回时使用,验证否有新的令牌生成,如果有取出新令牌并且将其返回)
package com.example.emos.aop;
import com.example.emos.common.util.R;
import com.example.emos.config.shiro.ThreadLocalToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect//切片类
@Component
public class TokenAspect {
@Autowired
private ThreadLocalToken threadLocalToken;
//拦截controller类里所有的web方法
@Pointcut("execution(public * com.example.emos.controller.*.*(..))")
public void aspect(){
}
@Around("aspect()")
public Object around(ProceedingJoinPoint point) throws Throwable{
//point.proceed();返回方法的执行结果
R r=(R)point.proceed();
String token=threadLocalToken.getToken();
//有新令牌
if(token!=null){
r.put("token",token);
threadLocalToken.clear();
}
return r;
}
}