后端选用springboot+mybatis进行开发,数据库采用mysql,预计会引入redis
项目架构
对于主要的代码文件,从上到下依次为:
- annotation: 自定义注解,如使用自定义注解实现身份token鉴权
- config: 全局设置,如配置拦截器在请求处理前获取并解析token
- context: 上下文,标识当前进行用户的一些信息,包含id、角色
- controller: api访问控制层,定义如何通过url访问到对应方法以及参数
- data:定义一些实例类型,分别为:
- constant:常量,多为枚举类型
- po:持久化存储对象,实际定义的对象需要存入数据库中
- vo:值对象,主要为不同层之间传递信息,一般封装一个Result对象用于返回
- handler: 全局处理类,如全局异常处理或websocket等方式的处理逻辑定义
- interceptor: 拦截器,定义具体对请求的前/后处理
- mapper: mybatis进行方法和sql的映射,底层
- service:具体处理请求的逻辑
- util: 工具类
项目按照这种形式规范进行开发。
token进行鉴权的流程
流程说明
token是标识一个用户的一种凭证,能够简化权限认证的过程,因此需要加密(防纂改),也需要设置凭证到期时效(增加安全性)。
从生成token开始,基本流程如下:
- 用户输入账号密码进行验证,若正确则需要生成token
- 提取需要的信息(如id、role)进行对称加密,生成有到期时间的token
- 分发token,用户后续请求携带token,根据token鉴权
从获取请求到从token获取信息,流程如下:
- 在请求到来时进行拦截,得到token
- 用加密密钥进行解密,得到token携带的payload信息
- 根据具体信息进行下一步操作,如根据权限判断是否合法
采用拦截器的主要原因是代码简洁,不需要在service层重复调用
加解密、token生成与提取信息
依赖
使用jwt库进行加密相关操作,pom.xml依赖配置:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
实现
utils/下配置一个简单的JWTUtil进行相关逻辑实现,可以直接复用
此处主要关注用户的id和role信息
token鉴权若无效或者过期,抛出AuthenticationException
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.time.Instant;
import java.util.Map;
import java.util.HashMap;
@Component
public class JWTUtil {
@Value("${jwt.token}")
private String SECRET_KEY;
@Value("${jwt.expiration}")
private Integer EXPIRATION;
public static String getToken(Map<String, String> map, int expireTime, String key) {
//获取时间,设置token过期时间
Instant instant = Instant.now();
Instant newInstant = instant.plusSeconds(expireTime);
//JWT添加payload
JWTCreator.Builder builder = JWT.create();
map.forEach(builder::withClaim);
//JWT过期时间 + signature
return builder.withExpiresAt(Date.from(newInstant)).sign(Algorithm.HMAC256(key));
}
public String getTokenWithPayLoad(String id, String role) {
Map<String, String> map = new HashMap<>();
map.put("id", id);
map.put("role", role);
return JWTUtil.getToken(map, EXPIRATION, SECRET_KEY);
}
// 验证token
public static void verify(String token, String key) {
JWT.require(Algorithm.HMAC256(key)).build().verify(token);
}
// 返回token内容
public static DecodedJWT getTokenInfo(String token, String key) {
return JWT.require(Algorithm.HMAC256(key)).build().verify(token);
}
}
拦截器逻辑及配置
注解
对于不同的方法,可能需要不同的权限进行访问控制,且在运行时进行确定
定义一个RequiredLogin注解判断权限是否合法
在annotations/下进行定义
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 方法注解
@Retention(RetentionPolicy.RUNTIME) // 运行时才能判断请求的权限合法性
public @interface RequiredLogin {
boolean required() default true;
String roles() default "ADMIN";
}
拦截器
重写Inteceptor方法,在preHandler下定义验证、解析并提取token信息
主要需要通过反射获取请求对应的方法,再判断是否有对应注解需要验证
import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import org.apache.catalina.connector.Response;
import org.example.backend.annotation.RequiredLogin;
import org.example.backend.context.UserContext;
import org.example.backend.data.constant.Role;
import org.example.backend.util.JWTUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.naming.AuthenticationException;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 拦截器,判断用户权限,返回值为true则放行,false则中止
*/
@Component
public class Interceptor implements HandlerInterceptor {
@Value("${jwt.token}")
private String SECRET_KEY;
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
// 判断调用的是不是一个接口方法
if(!(handler instanceof HandlerMethod)){
return true;
}
// 获取请求头里"token"内数据
String token = request.getHeader("token");
response.setContentType("application/json;charset=UTF-8");
// java反射
Method method = ((HandlerMethod) handler).getMethod();
// 如果类上有该注解,则执行
if(method.isAnnotationPresent(RequiredLogin.class)){
// 如果注解中required = true则执行
if(method.getAnnotation(RequiredLogin.class).required()){
// 获取需求的权限
String requiredRole = method.getAnnotation(RequiredLogin.class).roles();
try{
// 验证token
JWTUtil.verify(token, SECRET_KEY);
// 获取token的payload中信息
DecodedJWT info = JWTUtil.getTokenInfo(token, SECRET_KEY);
// 获取用户权限
String userRole = info.getClaim("role").asString();
UserContext.id = Integer.parseInt(info.getClaim("id").asString());
UserContext.role = userRole;
// 放行ADMIN
if (Objects.equals(userRole, Role.ADMIN.role)) return true;
// 权限需求为ALL则全放行
if (Objects.equals(requiredRole, "ALL")) return true;
// 进行权限比对
if(!Objects.equals(userRole, requiredRole)) throw new AuthenticationException();
return true;
}catch (AuthenticationException e){
response.sendError(Response.SC_FORBIDDEN, "Permission Denied");
return false;
} catch (Exception e){
response.sendError(Response.SC_UNAUTHORIZED, "Authentication failed");
return false;
}
}
}
return true;
}
}
拦截器配置
在configs/下声明InteceptorConfig:
可以配置多个拦截器
import jakarta.annotation.Resource;
import org.example.backend.interceptor.Interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
Interceptor interceptor;
/*
添加拦截器,进行token验证
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor)
.addPathPatterns("/**");
}
}
在配置完之后,只需要在方法上加上
@RequiredLogin(roles = "ALL")
则放行所有携带有效token的请求,需要对应权限修改ALL为对应权限名称即可,权限采用枚举类型
全局配置
除了拦截器的配置之外,也可以实现一些比较便捷的全局配置,如Transactional注解的方法若抛出错误则不会返回有效的请求,可以通过全局异常处理来捕获。
主要通过声明异常的类型进行捕获处理,如mybatis访问数据库出错,需要回滚,则不会在控制台抛出错误信息并中止请求,而是返回一个有意义的报错信息
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import org.apache.catalina.connector.Response;
import org.example.backend.data.vo.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/*
全局异常处理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MybatisPlusException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ResponseEntity<Object> handleDatabaseException(MybatisPlusException e) {
return ResponseEntity
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(Result.error(Response.SC_INTERNAL_SERVER_ERROR,"数据库异常,回滚:" + e.getMessage()));
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ResponseEntity<Object> handleException(Exception e) {
return ResponseEntity
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(Result.error(Response.SC_INTERNAL_SERVER_ERROR,"出现异常:" + e.getMessage()));
}
}
通过以上形式实现了一个简单有效、逻辑清晰的token鉴权访问机制,易读性比较好,后端会在此基础上进行下一步详细逻辑的开发