SpringCloud(八) - 自定义token令牌,鉴权(注解+拦截器),参数解析(注解+解析器)
1、项目结构介绍
项目有使用到,redis和swagger,不在具体介绍;
2、手动鉴权和用户信息参数获取(繁杂,冗余)
2.1用户实体类
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
//用户编号
private String userId;
//用户名
private String userName;
//用户密码
private String userPwd;
//手机号
private String userTel;
//邮箱
private String userEmail;
//登录ip
private String lastLoginIp;
}
2.2 业务层
2.2.1 接口
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户业务接口
*/
public interface UserService {
/**
* @author : huayu
* @date : 4/11/2022
* @param : [userName, userPwd]
* @return : java.lang.String
* @description : 处理用户登录请求,校验用户信息是否正确,如果正确返回令牌
*/
String userLogin(String userName,String userPwd);
/**
* @author : huayu
* @date : 5/11/2022
* @param : [userToken]
* @return : void
* @description : 用户登出
*/
void userLogout(String userToken);
}
2.2.3 实现类
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户业务接口实现类
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RedisUtils redisUtils;
@Override
public String userLogin(String userName, String userPwd) {
//TODO 调用持久层接口,查询用户信息是否真确,如果查询到用户信息,说明用户存在,如果查询不到没说明用户不存在
if("KH96".equals(userName) && "123456".equals(userPwd)){
//代表用户的登录信息是正确的,可以生成token令牌,返回该客户端
//令牌的生成规则,一般是随机串,长度不一,一般使用方式:可以选择UUID生成,或者将用户编号+其他信息进行md5加密,比如jwt
String userToken = UUID.randomUUID().toString().replace("-", "");
//简单模拟数据库查询出的用户详情
User userLogin = User.builder()
.userId("T001")
.userName("KH96")
.userTel("13801020304")
.userEmail("kh97@kgc.com")
.lastLoginIp("127.0.0.1")
.build();
//将查询的用户详情,直接一生成的token作为key,存入到redis缓存中,并增加时效(有过期时间,比如30分钟)
redisUtils.set(userToken,userLogin,10*60);
//返回有效的token令牌,此令牌就代表登录成功的用户
return userToken;
}
//鉴权失败,返回null;
return null;
}
@Override
public void userLogout(String userToken) {
// 直接将用户的token令牌长redis中删除
if(redisUtils.hasKey(userToken)){
redisUtils.del(userToken);
}
}
}
2.3 控制层
2.3.1 BaseController
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 所有控制器的供父类,将所有有控制器要使用的公共方法,抽离到父类中,方便方法复用
*/
public class BaseController {
/**
* @author : huayu
* @date : 4/11/2022
* @param : [request, paramName]
* @return : java.lang.String
* @description : 从请求中获取参数,获取参数值,如果没有获取到,用空字符地带默认值的null
*/
protected String getParameter(HttpServletRequest request,String paramName){
return request.getParameter(paramName) == null ? "" : request.getParameter(paramName);
}
protected String getParameter(HttpServletRequest request,String paramName,String defaultValue){
return request.getParameter(paramName) == null ? defaultValue : request.getParameter(paramName);
}
/**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : java.lang.String
* @description : getRemoteIp
*/
protected String getRemoteIp(HttpServletRequest request) {
// 获取ip
String ip = request.getHeader("X-Real-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
2.3.2 LoginController
用户登录:
- 根据用户名密码判断用户是否存在
- 存在生成token,返回给前端;不存在提示用户名或密码错误;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户登录登出
*/
@Slf4j
@RestController
@Api(tags = "用户登录登出类")
public class LoginController extends BaseController {
@Autowired
private UserService userService;
@PostMapping("/login")
@ApiOperation(value = "用户登录",notes = "支持token鉴权")
@ApiImplicitParams({
@ApiImplicitParam(value = "用户名",name = "userName",defaultValue = "KH96"),
@ApiImplicitParam(value = "用户密码",name = "userPwd",defaultValue = "123456")
})
public RequestResult<String> doLogin(HttpServletRequest request){
//获取请求中的用户名和密码参数
String loginName = this.getParameter(request, "userName", "KH96");
String loginPwd = this.getParameter(request, "userPwd", "123456");
//调用业务接口,校验登录请求用户信息是否正确,如果正确,返回token令牌,否者返回null
String userToken = userService.userLogin(loginName, loginPwd);
//判断用户是否鉴权成功
if(StringUtils.isNotBlank(userToken)){
//登录鉴权成功,返回给客户端有限token令牌,前端保存,后续请求使用
return ResultBuildUtil.success(userToken);
}
return ResultBuildUtil.fail("901","用户名或密码错误!");
}
}
2.3.3 UserController
收藏列表查询:
- 看请求头参数中是否携带正确的token,进行鉴权
- 鉴权成功获取用户信息,查询对应数据,鉴权失败,跳转到用户登录页面;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户操作入口
*/
@Slf4j
@RestController
@Api(tags = "用户个人中心")
public class UserController {
@Autowired
private RedisUtils redisUtils;
/**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户查询收藏列表,需要 token鉴权操作
*/
@GetMapping("/collectionList")
@ApiOperation(value = "收藏列表",notes = "支持token自动鉴权")
public RequestResult<String> collectList(HttpServletRequest request){
//直接获取前端请求的token参数进行鉴权操作,省略业务层接口操作
String userToken = request.getHeader("token");
//判断token是否合法,如果没有直接鉴权失败,跳转到登录
if(StringUtils.isBlank(userToken)){
//返回 鉴权失败
return ResultBuildUtil.fail("902","token参数为空,请求失败,请求重新登录");
}
//判断token是否有效,如果redis中可以根据此token获取到信息,说明用户登录成功,且有效,否者鉴权失败,跳转到登录
Object userObj = redisUtils.get(userToken);
if(ObjectUtils.isEmpty(userObj)){
//redis中没有该token的鉴权信息,饭后鉴权失败
return ResultBuildUtil.fail("903","token 参数失效,请重新登录!");
}
//请求token值有效,直接将redis中存放的用户信息,转换为登录用户详情
User loginUser = JSON.parseObject(userObj.toString(), User.class);
//TODO 将鉴权通过的用户信息作为信息,调用查询用户收藏列表业务接口,获取该用户的收藏信息,返回给前端
log.info("------ 用户查看收藏列表,鉴权通过,当前登录用户:{} ------",loginUser);
//返货成功的收藏列表数据
return ResultBuildUtil.success("查询用户收藏列表成功!\n "+loginUser);
}
}
2.4 测试
2.4.1 测试用户登录
2.4.1.1 用户登陆成功
2.4.1.2 用户token添加成功
2.4.2 测试查询用户收藏信息
2.4.2.1 使用错误的token
2.4.2.2 使用正确的token
2.5 总结
虽然业务可以完成,但是每次都进行这样的手动鉴权和手动获取用户数据,比较繁琐,而且大量代码冗余;
3、自动鉴权和自动用户信息参数获取
3.1 原理
- 自动鉴权
- 自定义注解+自定义拦截器
- 自动参数获取
- 自定义注解+自定义解析器
3.2 自定义注解
3.2.1 自定义token鉴权注解
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 请求token许可自定义注解,只要请求处理方法上加了此注解,就需要token鉴权
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPermission {
}
3.2.2 自定义参数解析(获取)注解
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求用户注解,凡是在目标请求处理方法中,使用此注解,就自动解析redis中保存的登录用户,绑定到实体属性上
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestUser {
}
3.3 自定义请求token许可拦截器
- 判断目标请求方法是否需要鉴权,是返回true,否发false
- 判断目标请求方法上是否有 添加了 请求token许可注解 @RequestPermission;
- 判断目标请求类方法上是否 添加了 请求token许可注解 @RequestPermission;
- 鉴权
- 鉴权成功不拦截;
- 鉴权失败拦截;
回顾过滤器和拦截器的执行时机:
过滤器是在DispatcherServlet
处理之前拦截,拦截器是在DispatcherServlet
处理请求然后调用控制器方法(即我们自己写的处理请求的方法,用@RequestMapping
标注)之前进行拦截。
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求token许可拦截器,拦截所有增加了请求token许可注解的请求,进行鉴权操作
*/
@Slf4j
public class TokenPermissionInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object obj) throws Exception {
// 判断是否需要校验请求token许可,只需要看目标请求处理方法上是否有自定义请求token许可注解-TokenPermission
if (this.checkTargetMethodHasTokenPermission(obj)){
// 需要进行请求token许可校验,从请求头中获取token参数,做token鉴权业务逻辑处理
String userToken = httpServletRequest.getHeader("token");
// 判断token是否合法,如果没有,直接鉴权失败,跳转到登录
if(StringUtils.isBlank(userToken)){
// token参数为空,返回鉴权失败
this.returnTokenCheckJson(httpServletResponse, "902", "token参数为空,鉴权失败,请重新登录!");
// 权限校验失败,需要拦截请求
return false;
}
// 判断token是否有效,如果redis中可以根据此token值获取到信息,说明用户登录鉴权成功,且有效,否则鉴权失败,跳转到登录
if(ObjectUtils.isEmpty(redisUtils.get(userToken))){
// redis中没有该token的鉴权信息,返回鉴权失败
this.returnTokenCheckJson(httpServletResponse, "903", "token参数失效,鉴权失败,请重新登录!");
// 权限校验失败,需要拦截请求
return false;
}
}
// 不需要拦截,直接放行
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception ex) throws Exception {
}
/**
* @author : zhukang
* @date : 2022/11/4
* @param : [handler]
* @return : boolean
* @description : 判断目标请求方法是否需要鉴权,是返回true,否发false
*/
public boolean checkTargetMethodHasTokenPermission(Object handler){
// 判断当前处理的handler是否已经映射到目标请求处理方法,看是不是HandlerMethod的实例对象
if(handler instanceof HandlerMethod){
// 强转为目标请求处理方法的实例对象,因为:HandlerMethod对象封装了目标请求处理方法的所有内容,包括方法所有的声明
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 尝试获取目标请求处理方法上,是否添加了自定义请求token许可注解-TokenPermission,取到了就是加了,取不到就没加
RequestPermission requestPermission = handlerMethod.getMethod().getAnnotation(RequestPermission.class);
// 判断是否成功获取到请求token许可注解,如果没有获取到,不一定代表不需要进行权限校验,因为此注解还可能加载处理类,要再次尝试从请求处理方法所在处理类上获取该注解
if(ObjectUtils.isEmpty(requestPermission)){
requestPermission = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequestPermission.class);
}
// 最终判断是否需要进行请求token许可校验,如果获取到了,说明需要校验,否则直接放行
return null != requestPermission;
}
// 请求不是需要进行鉴权操作,直接返回false
return false;
}
/**
* @author : zhukang
* @date : 2022/11/4
* @param : [response, returnCode, returnMsg]
* @return : void
* @description : 拦截器中,token鉴权失败的统一返回json处理
*/
public void returnTokenCheckJson(HttpServletResponse response, String returnCode, String returnMsg){
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
try {
response.getWriter().print(JSON.toJSONString(ResultBuildUtil.fail(returnCode, returnMsg)));
} catch (IOException e) {
log.warn("****** 请求token许可拦截器返回结果异常:{} ******", e.getMessage());
}
}
}
3.4 自定义请求用户参数解析器
通过鉴权后:
- 判断 目标请求处理方法是否 自定义参数解析注解@RequestUser,且目标实体参数类型是User;
- 通过token为key取用redis中的用户信息;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求用户参数解析器,自动根据 @RequestUser 注解,解析通过鉴权的用户信息,绑定到请求处理方法的用户参数上,要配合请求token许可鉴权使用
*/
public class MyDefineUserResolver implements HandlerMethodArgumentResolver {
@Autowired
private RedisUtils redisUtils;
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 决定是否需要执行参数解析,如果目标请求处理方法使用了自定义参数注解@RequestUser,且目标实体参数类型是User,就需要进行解析,否则不需要解析
return parameter.hasParameterAnnotation(RequestUser.class) && parameter.getParameterType().isAssignableFrom(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 根据上面supportsParameter方法,如果返回的是true,代表需要执行方法参数解析,如果返回false,不需要执行参数解析
// 从redis中获取token令牌保存的用户信息,转换为目标用户对象,绑定到请求处理方法的入参中,前提:鉴权是通过
// TODO 在获取redis中保存的用户信息时,需要做非空校验,防止解析时过期
return JSON.parseObject(redisUtils.get(webRequest.getHeader("token")).toString(), User.class);
}
}
3.5 自定义webmvc配置类
- 手动创建请求token许可拦截器对象,放入容器
- 手动添加自定义拦截器到系统的拦截器组中;
- 手动创建自定义解析器对象,放入容器
- 手动添加自定义拦截器到系统的拦截器组中;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义webmvc配置类,可以自定义
*/
@Configuration
public class MyDefineWebMVcConfig implements WebMvcConfigurer {
/**
* @author : huayu
* @date : 4/11/2022
* @param : []
* @return : com.kgc.scd.interceptor.TokenPermissionInterceptor
* @description : 手动创建请求token许可拦截器对象,放入容器,方便加入到系统拦截器组中
*/
@Bean
public TokenPermissionInterceptor tokenPermissionInterceptor(){
return new TokenPermissionInterceptor();
}
/**
* @author : huayu
* @date : 4/11/2022
* @param : []
* @return : com.kgc.scd.resolver.MyDefineUserResolver
* @description : 手动创建自定义解析器对象,放入容器,方便加入到系统解析器中
*/
@Bean
public MyDefineUserResolver myDefineUserResolver(){
return new MyDefineUserResolver();
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
@Override
public void addFormatters(FormatterRegistry registry) {
}
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
//手动添加自定义拦截器到系统的拦截器组中,才可以生效,否者不生效
interceptorRegistry.addInterceptor(tokenPermissionInterceptor()).addPathPatterns("/**");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
@Override
public void addCorsMappings(CorsRegistry registry) {
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
//手动将容器中自定义请求用户解析器,加入到系统解析器中
argumentResolvers.add(myDefineUserResolver());
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
3.5 UserController
- 在方法上或类上添加 自定义请求token许可注解 @RequestPermission ;
- 进行用户token自动鉴权;
- 在参数添加 自定义参数解析注解 @RequestUser
- 进行用户类型参数自动解析;(通过健全后,自动获取用户参数)
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户操作入口
*/
@Slf4j
@RestController
@Api(tags = "用户个人中心")
@RequestPermission //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权; 如果在类上增加了此注解,就地表当前类的所有处理方法都需要鉴权;
public class UserController {
@Autowired
private RedisUtils redisUtils;
/**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户查询 足迹列表,需要 token鉴权操作
*/
@GetMapping("/footList")
@ApiOperation(value = "足迹列表",notes = "支持token自动鉴权")
@RequestPermission //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权
public RequestResult<String> footList(@RequestUser @ApiIgnore User loginUser){
//TODO 当遇到需要进行token鉴权操作,就必须重复上面的收藏鉴权操作,代码冗余,不利于扩展和维护
//TODO 推荐用法:使用自定义实现自动鉴权,当添加了需要进行鉴权的自定义注解,执行鉴权操作,如果没添加则不需要
//TODO 如果token鉴权成功,直接获取用户信息,调用业务接口,查询用户的足迹列表数据,返回前端
log.info("------ 用户查看足迹列表,鉴权通过,当前登录用户:{} ------",loginUser);
//返回成功的收藏列表数据
return ResultBuildUtil.success("查询用户足迹列表成功!"+loginUser);
}
}
3.6 LoginController 用户登出
/**
* @author : huayu
* @date : 4/11/2022
* @param : [token]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户退出登录
*/
@GetMapping("/logout")
@ApiOperation(value = "用户登出", notes = "用户删除token,退出系统")
public RequestResult<String> doLogout(@RequestHeader String token){
// 调用业务接口,删除用户的token令牌
userService.userLogout(token);
return ResultBuildUtil.success("退出登录成功!");
}
3.7 测试
3.7.1 测试获取用户足迹
3.2.1.1 使用错误的token
3.2.1.2 使用正确的token
3.7.2 测试用户登出
3.7.2.1 用户登出成功
3.7.2.2 用户token被删除
3.8 总结
使用自定义鉴权注解 自动鉴权,和自定义参数解析注解 自动获取参数;代码量大大减少,而且操作方便;