SpringBoot集成JWT实现权限验证
技术概述
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON
的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON
对象的形式安全的传递信息。由于完成的项目需要进行不同身份用户的访问权限验证,因此采用的JWT实现验证。
技术详述
总体思路
在项目中的设计的思路如下图所示,首先是在登录验证的函数接口不设置权限(具体权限怎么设置下文会讲),经过验证后将数据返回前端(本项目业务需要这里返回的是用户视图对象-UserVO,数据类型采用JSON数据类型,在账户数据中添加了token字段保存),前端在获取数据后将token进行保存,在后续的访问中将token加入访问的请求头中,经过解析token验证用户访问是否满足访问函数要求的权限,决定是否拒绝访问请求。
public class UserVO{
private User user;
private AccountData accountData;
private String token;
}
TokenService是根据用户信息(这里是通过id和password)动态生成token,加密算法是HMAC256,这样可以用于后续如果两个用户同时异地登录但是一端在使用过程中修改密码,另一端也被迫中止访问的业务需求。读者可以根据具体情况设计自己需要的token生成方法。
@Service("TokenService")
public class TokenService {
public String getToken(User user) {
String token="";
token= JWT.create().withAudience(user.getId().toString())//将user.id保存到token里面
.sign(Algorithm.HMAC256(user.getPassword()));//以password作为 token 的密钥
return token;
}
}
TokenService中的JWT是通过导入com.auth0.jwt.JWT;包引入的类,maven依赖如下
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
为访问接口添加权限
这里通过自定义注解的方式为访问类/方法添加权限,下面这个是我自定义的一个管理员权限的注解,@Target的内容可以修改,修改后可以更改权限添加的位置(是类还是方法等等)
/**
* 需要管理员权限的注释
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminLimit {
boolean required() default true;
}
我这里的是权限设置的是方法,我把注释放在了controller的访问接口上面,这里添加了两个注解分别对应了后面注解的权限,在前端访问该接口的时候会经过过滤器验证,过滤器在下面会给出。
//管理员界面获取奖励申请记录列表(具体实现)
@LoginToken//需要登录
@AdminLimit//管理员权限
@GetMapping("/rewards")
public @ResponseBody List<RewardVO> getRewards(){
return rewardService.getRewardList();
}
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention:注解的保留位置
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解
权限验证过滤器
下面是我在项目中设置的权限过滤器,通过request.getHeader方法获取前端在请求头中设置的token变量,以为token是根据用户id和password生成的(忘记的朋友可以到上面翻一下,通过TokenService生成),通过JWT.decode(token).getAudience().get(0)获取用户的id,通过userService用id从数据库中获取用户信息,若通过验证返回true会进行后续的访问,否则返回false终止访问,根据错误情况不同通过response.setStatus设置错误状态码,方便前端根据不同的状态码作出不同的反应。
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
String token = request.getHeader("token");// 从 http 请求头中取出 token
User user = null;//登录用户
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passToken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//判断是否需要登录权限
if(method.isAnnotationPresent(LoginToken.class)){
LoginToken userLoginToken = method.getAnnotation(LoginToken.class);
if (userLoginToken.required()) {
//执行认证
if(token == null){
//未登录用户
//设置响应状态码
response.setStatus(ErrorStatus.NOT_LOGGED_IN);
//停止后续访问
return false;
}else{//有token
String userId;
try {//根据token获取uid
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//错误的token
response.setStatus(ErrorStatus.BAD_TOKEN);
return false;
}
user = userService.getUserById(Integer.parseInt(userId));
if(user == null){
//用户不存在
//设置响应状态码
response.setStatus(ErrorStatus.ACCOUNT_NOT_EXIT);
//终止后续访问
return false;
}else{
String token2 = tokenService.getToken(user);
if(!token2.equals((token))){
response.setStatus(ErrorStatus.PASSWORD_ERROR);
return false;
}
}
}
}
}
//检查是否需要管理员权限
if(method.isAnnotationPresent(AdminLimit.class)){
if(!user.getIdentity().equals(UserIdentity.admin)){//没有管理员权限
//设置响应状态码
response.setStatus(ErrorStatus.BEYOND_IDENTITY_LIMIT);
//终止后续访问
return false;
}
}
if(method.isAnnotationPresent(UserLimit.class)){//需要普通用户权限
if(!user.getIdentity().equals(UserIdentity.student)
&& !user.getIdentity().equals(UserIdentity.teacher)){
//既不是老师也不是学生身份
//设置响应状态码
response.setStatus(ErrorStatus.BEYOND_IDENTITY_LIMIT);
//终止后续访问
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
HandlerInterceptor接口主要定义了三个方法
1.boolean preHandle ():
预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行
postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。
2.void postHandle():
后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
3.void afterCompletion():
整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
配置拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
感谢您的浏览,如果有说明不当之处欢迎指出,谢谢