这部分就简单了点,用的不是 Shiro,只有认证没有授权。
用户登录
登陆成功后根据用户id创建token,并保存至数据库,同时返回token信息,下次请求会将token放入之请求头
@PostMapping("login")
@ApiOperation("登录")
public Result<Map<String, Object>> login(@RequestBody LoginDTO dto) {
//表单校验
ValidatorUtils.validateEntity(dto);
//用户登录
Map<String, Object> map = userService.login(dto);
return new Result().ok(map);
}
public Map<String, Object> login(LoginDTO dto) {
UserEntity user = getByMobile(dto.getMobile());
AssertUtils.isNull(user, ErrorCode.ACCOUNT_PASSWORD_ERROR);
//密码错误
if (!user.getPassword().equals(DigestUtil.sha256Hex(dto.getPassword()))) {
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
}
//获取登录token
TokenEntity tokenEntity = tokenService.createToken(user.getId());
Map<String, Object> map = new HashMap<>(2);
map.put("token", tokenEntity.getToken());
map.put("expire", tokenEntity.getExpireDate().getTime() - System.currentTimeMillis());
return map;
}
自定义权限拦截器,进行权限验证
很多Controller方法,刚进来要先获取当前登录用户的信息,以便做后续的用户相关操作,如果每个Controller开始,先调用tokenUtils.getUserByToken(token),不够优雅。所以使用的mvc拦截器HandlerInterceptor+方法参数解析器HandlerMethodArgumentResolver最合适。
SpringMVC提供了mvc拦截器HandlerInterceptor,包含以下3个方法:
- preHandle
- postHandle
- afterCompletion
HandlerInterceptor经常被用来解决拦截事件,如用户鉴权等。另外,Spring也向我们提供了多种解析器Resolver,如用来统一处理异常的HandlerExceptionResolver,以及今天的主角HandlerMethodArgumentResolver。HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:
- supportsParameter(满足某种要求,返回true,方可进入resolveArgument做参数处理)
- resolveArgument
知识储备已到位,接下来着手实现,主要分为三步走:
- 自定义权限拦截器 AuthorizationInterceptor 拦截所有request请求,进行鉴权认证操作,成功后将token解析为当前用户信息TokenEntity,最终放到request中;
- 自定义参数注解@LoginUser,添加至controller的方法参数user之上;
- 自定义方法参数解析器CurrentUserMethodArgumentResolver,取出request中的user,并赋值给添加了@LoginUser注解的参数user。
自定义权限拦截器AuthenticationInterceptor,需实现HandlerInterceptor。在preHandle中调用tokenService.getByToken(token),获取到当前用户,最后塞进request中,如下:
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Resource
private TokenService tokenService;
public static final String USER_KEY = "userId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Login annotation;
if (handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
} else {
return true;
}
if (annotation == null) {
return true;
}
//从header中获取token
String token = request.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StrUtil.isBlank(token)) {
token = request.getParameter("token");
}
//token为空
if (StrUtil.isBlank(token)) {
throw new RenException(ErrorCode.TOKEN_NOT_EMPTY);
}
//查询token信息
TokenEntity tokenEntity = tokenService.getByToken(token);
if (tokenEntity == null || tokenEntity.getExpireDate().getTime() < System.currentTimeMillis()) {
throw new RenException(ErrorCode.TOKEN_INVALID);
}
//设置userId到request里,后续根据userId,获取用户信息
request.setAttribute(USER_KEY, tokenEntity.getUserId());
return true;
}
}
自定义参数注解
自定义方法参数上使用的注解@LoginUser,代表被它注解过的参数的值都需要由方法参数解析器LoginUserHandlerMethodArgumentResolver 来“注入”,如下:
/**
* 登录用户信息
*
* @author Mark sunlightcs@gmail.com
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
给各Controller类中RequestMapping方法的参数添加此注解,如下:
@Login
@GetMapping("userInfo")
@ApiOperation(value="获取用户信息", response=UserEntity.class)
public Result<UserEntity> userInfo(@ApiIgnore @LoginUser UserEntity user){
return new Result<UserEntity>().ok(user);
}
自定义方法参数解析器
自定义方法参数解析器 LoginUserHandlerMethodArgumentResolver ,需实现HandlerMethodArgumentResolver
@Component
@AllArgsConstructor
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
//获取用户ID
Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
if (object == null) {
return null;
}
//获取用户信息
UserEntity user = userService.getUserByUserId((Long) object);
return user;
}
}
最后配置MVC
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AuthorizationInterceptor authorizationInterceptor;
@Resource
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new ResourceHttpMessageConverter());
converters.add(new AllEncompassingFormHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(jackson2HttpMessageConverter());
}
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
//日期格式转换
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
//Long类型转String类型
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
mapper.registerModule(simpleModule);
converter.setObjectMapper(mapper);
return converter;
}
}
总结一下app认证流程。
- app模块登录,验证登录信息后创建token
- 请求需要登录权限的方法,进入
AuthorizationInterceptor
进行token验证 - 确认已登录,如果方法需要登录信息,从 request 域中获取