文章目录
前期准备
jwt,我的理解就是可以进行客户端与服务端之间验证的一种技术,取代了之前使用Session来验证的不安全性。为什么不适用Session?
原理是,登录之后客户端和服务端各自保存一个相应的SessionId,每次客户端发起请求的时候就得携带这个SessionId来进行比对
- Session在用户请求量大的时候服务器开销太大了
- Session不利于搭建服务器的集群(也就是必须访问原本的那个服务器才能获取对应的SessionId)
- 小程序,APP不适用session,对于微信小程序,request请求每次都会先请求微信的服务器,再由微信的服务器去访问我们的后端服务器。这样子就不能识别出是哪台浏览器发送的请求。
1.首先,登录的话,不能在通过shiro自己的方法去验证了,因为我们自定义的token需要存储用户使用的token,所以登录的密码验证就不能通过shiro进行验证,而需要我们自己去验证密码的准确性,在登录方法里面可以,在realm认证方法里面也可以.
2.然后,基于token进行权限验证的话,我们请求所有需要认证的接口时候请求头里必须携带token,然后后端进行token认证,判断token是否合法是否过期等等…
3.token的刷新,可以自定义返回code,返回新的token,来进行token刷新工作.
4.token缓存在redis中,可以实现集群token的共享.可以使token的过期删除交给redis
在这里我先不涉及redis,单纯的jwt结合shiro,所以也没有做token的过期处理,登出处理,即使你使用shiro的logout方法登出,这里的token依然没有失效
由于使用jwt是无状态的,不需要使用session,所以需要把session关闭掉。具体的编码过程如下。
个人理解:
- jwt负责生成token,取代shiro原生的UsernamePasswordToken;shiro负责认证和权限的校验
- 登录逻辑沿用jwt的登录逻辑,即登录时不需要调用shiro的subject.login()方法,只需要校验用户名和密码,然后返回token即可。到了需要进行权限认证时在执行login方法,这里使用的是jwtFilter来进行拦截。
这个工作的流程有,登录,权限控制:
- 登录,登录还是做简单的接受请求传递过来的参数,然后和数据库对比,是否一致,一致的话则通过登录,使用jwt生成token,不经过shiro的处理,因为被jwtFilter拦截了。
- 认证,认证的话先是被jwtFilter拦截,然后验证token,无异常就继续进入到shiro
- 如果是只要拥有登录权限的话,那么就经过认证方面就可以了
- 如果是要控制权限的话,那么就要先认证再授权
有兴趣看源码的话可以去我的项目地址clone下来:
项目地址:https://github.com/HTBWell/shiro-jwt.git
1. 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
2. 编写JwtUtil类
JwtUtil类是用来生成token和验校验解码token的。
步骤:
- 设置密钥和token的有效时间
- 生成token
- 校验token
- 获取token的信息
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.demo.domain.User;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtil {
//token有效时长
private static final long EXPIRE=30*60*1000L;
//token的密钥
private static final String SECRET="jwt+shiro";
public static String createToken(User user) throws UnsupportedEncodingException {
//token过期时间
Date date=new Date(System.currentTimeMillis()+EXPIRE);
//jwt的header部分
Map<String ,Object>map=new HashMap<>();
map.put("alg","HS256");
map.put("typ","JWT");
//使用jwt的api生成token
String token= JWT.create()
.withHeader(map)
.withClaim("username", user.getUsername())//私有声明
.withExpiresAt(date)//过期时间
.withIssuedAt(new Date())//签发时间
.sign(Algorithm.HMAC256(SECRET));//签名
return token;
}
//校验token的有效性,1、token的header和payload是否没改过;2、没有过期
public static boolean verify(String token){
try {
//解密
JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
verifier.verify(token);
return true;
}catch (Exception e){
return false;
}
}
//无需解密也可以获取token的信息
public static String getUsername(String token){
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
3. 封装token
封装token来替换Shiro原生Token,要实现AuthenticationToken接口
shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。
import org.apache.shiro.authc.AuthenticationToken;
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token){
this.token=token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
4. 编写JWT的过滤器
这个过滤器是我们的重点,这里我们继承的是Shiro内置的BasicHttpAuthenticationFilter
,一个可以内置了可以自动登录方法的的过滤器。也可以继承AuthenticatingFilter
。我这里两个都实现了,跑项目的时候只要一个就好了
这个过滤器是要注册到shiro配置里面去的,用来辅助shiro进行过滤处理。所有的请求都会到过滤器来进行处理。
我们需要重写几个方法:
isAccessAllowed
:是否允许访问。如果带有 token,则对 token 进行检查,否则直接通过。如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 trueisLoginAttempt
:判断用户是否想要登入。检测 header 里面是否包含 Token 字段。executeLogin
:executeLogin
实际上就是先调用createToken
来获取token,这里我们重写了这个方法,就不会自动去调用createToken来获取token,然后调用getSubject方法来获取当前用户再调用login方法来实现登录,这也解释了我们为什么要自定义jwtToken,因为我们不再使用Shiro默认的UsernamePasswordToken
了。preHandle
:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
- 继承
BasicHttpAuthenticationFilter
import com.example.demo.shiro.JWTToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;