一、技术概述
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();
}
权限验证过滤器
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 {
}
}
配置拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}