1. 使用Token进行身份鉴权

网站应用一般使用Session进行登录用户信息的存储及验证,而在移动端使用Token则更加普遍。它们之间并没有太大区别,Token比较像是一个更加精简的自定义的Session。Session的主要功能是保持会话信息,而Token则只用于登录用户的身份鉴权。所以在移动端使用Token会比使用Session更加简易并且有更高的安全性,同时也更加符合RESTful中无状态的定义。

2. 交互流程
  • 客户端通过登陆接口提交用户名密码
  • 服务端检查后生成token,并与用户关联(这里使用redis缓存起来)
  • 客户端在之后的请求都携带token,服务端通过token检查用户身份

    3. 实现
  • Token实体

  1. public class TokenEntity {
  2. private String userId;
  3. private String token;
  4. //忽略构造方法和setter/getter
  5. }
  • User实体
  1. @Entity
  2. @Table(name = "user")
  3. public class UserEntity {
  4. @Id
  5. @GeneratedValue(generator = "system-uuid")
  6. @GenericGenerator(name = "system-uuid", strategy = "uuid")
  7. @Column(name = "user_id", unique = true, nullable = false, length = 32)
  8. private String userId;
  9. @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
  10. private Date createDate;
  11. private String userName;
  12. private String password;
  13. private boolean dr;
  14. //忽略构造方法和setter/getter
  • 用于管理Token的服务接口
  1. public interface TokenService {
  2. /**
  3. * 创建一个token关联上指定用户
  4. *
  5. * @param userId 指定用户的id
  6. * @return 生成的token
  7. */
  8. TokenEntity createToken(String userId);
  9. /**
  10. * 检查token是否有效
  11. *
  12. * @param model token
  13. * @return 是否有效
  14. */
  15. boolean checkToken(TokenEntity model);
  16. /**
  17. * 从字符串中解析token
  18. *
  19. * @param authentication 加密后的字符串
  20. * @return Token实例
  21. */
  22. TokenEntity getToken(String authentication);
  23. /**
  24. * 清除token
  25. *
  26. * @param userId 登录用户的id
  27. */
  28. void deleteToken(String userId);
  29. }
  • 使用Reids管理Token的TokenService 实现类
  1. package com.sukaiyi.demo.certification.service.impl;
  2. import com.sukaiyi.demo.certification.constants.Constants;
  3. import com.sukaiyi.demo.certification.entity.TokenEntity;
  4. import com.sukaiyi.demo.certification.service.TokenService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.data.redis.core.RedisTemplate;
  7. import org.springframework.stereotype.Component;
  8. import org.springframework.util.StringUtils;
  9. import java.util.UUID;
  10. import java.util.concurrent.TimeUnit;
  11. /**
  12. * @author sukaiyi
  13. */
  14. @Component
  15. public class RedisTokenService implements TokenService {
  16. @Autowired
  17. RedisTemplate<String, TokenEntity> redisTemplate;
  18. @Override
  19. public TokenEntity createToken(String userId) {
  20. String token = UUID.randomUUID().toString();
  21. TokenEntity tokenEntity = new TokenEntity(userId, token);
  22. redisTemplate.opsForValue().set(userId, tokenEntity, Constants.TOKEN_EXPIRES_HOUR, TimeUnit.MINUTES);
  23. return tokenEntity;
  24. }
  25. @Override
  26. public boolean checkToken(TokenEntity entity) {
  27. if (entity == null) {
  28. return false;
  29. }
  30. TokenEntity token = redisTemplate.opsForValue().get(entity.getUserId());
  31. if (token == null || StringUtils.isEmpty(token.getToken())) {
  32. return false;
  33. }
  34. return token.getToken().equals(entity.getToken());
  35. }
  36. @Override
  37. public TokenEntity getToken(String authentication) {
  38. // userId 为32位字符串
  39. // userId拼接token得到authentication
  40. // 所以要求authentication长度大于32
  41. if (!StringUtils.isEmpty(authentication) && authentication.length() > 32) {
  42. TokenEntity tokenEntity = new TokenEntity();
  43. String userId = authentication.substring(0, 32);
  44. String token = authentication.substring(32);
  45. tokenEntity.setUserId(userId);
  46. tokenEntity.setToken(token);
  47. return tokenEntity;
  48. }
  49. return null;
  50. }
  51. @Override
  52. public void deleteToken(String userId) {
  53. redisTemplate.delete(userId);
  54. }
  55. }
  • 登陆/注销的RESTful接口
  1. @RestController
  2. @RequestMapping("/tokens")
  3. public class TokenController {
  4. @Autowired
  5. private UserRepository userRepository;
  6. @Autowired
  7. private TokenService tokenService;
  8. @RequestMapping(method = RequestMethod.POST)
  9. public ResponseEntity login(@RequestParam String userName, @RequestParam String password) throws BusinessException {
  10. if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
  11. throw new BusinessException(ExceptionCode.NEED_FIELD, "用户名或密码为空");
  12. }
  13. UserEntity user = userRepository.findUserByUserName(userName);
  14. if (user == null) {
  15. throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");
  16. }
  17. if (MD5Util.encode(password).equals(user.getPassword())) {
  18. TokenEntity tokenEntity = tokenService.createToken(user.getUserId());
  19. return new ResponseEntity<>(ResultEntity.ok(tokenEntity.getUserId() + tokenEntity.getToken()), HttpStatus.OK);
  20. }
  21. throw new BusinessException(ExceptionCode.AUTHORIZATION_FAILED, "用户名或密码错误");
  22. }
  23. @Authorization
  24. @RequestMapping(method = RequestMethod.DELETE)
  25. public ResponseEntity logout(@CurrentUser UserEntity user) {
  26. tokenService.deleteToken(user.getUserId());
  27. return new ResponseEntity<>(ResultEntity.ok("注销成功"), HttpStatus.OK);
  28. }
  29. }

其中logout方法的有关的两个注解@Authorization@CurrentUser:
@Authorization标注在controller的rest方法上,表示访问这个资源需要登陆授权;

  1. /**
  2. * 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误
  3. * @author sukaiyi
  4. */
  5. @Target(ElementType.METHOD)
  6. @Retention(RetentionPolicy.RUNTIME)
  7. public @interface Authorization {
  8. }

@CurrentUser标注在方法的参数上,用于自动将当前登陆用户注入到该参数。

  1. /**
  2. * 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
  3. *
  4. * @author sukaiyi
  5. */
  6. @Target(ElementType.PARAMETER)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface CurrentUser {
  9. }
  • 处理Authorization注解的拦截器
  1. @Component
  2. public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
  3. @Autowired
  4. private TokenService tokenService;
  5. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
  6. if (!(handler instanceof HandlerMethod)) {
  7. return true;
  8. }
  9. HandlerMethod handlerMethod = (HandlerMethod) handler;
  10. Method method = handlerMethod.getMethod();
  11. if (method.getAnnotation(Authorization.class) == null){
  12. return true;
  13. }
  14. String authorization = request.getHeader(Constants.AUTHORIZATION);
  15. TokenEntity tokenEntity = tokenService.getToken(authorization);
  16. if (tokenService.checkToken(tokenEntity)) {
  17. //如果token验证成功,将token对应的用户id存在request中,便于之后注入
  18. request.setAttribute(Constants.CURRENT_USER_ID, tokenEntity.getUserId());
  19. return true;
  20. }else{
  21. response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  22. return false;
  23. }
  24. }
  25. }

拦截器配置为Bean,并添加拦截器

  1. @SpringBootApplication
  2. public class DemoApplication extends WebMvcConfigurerAdapter {
  3. public static void main(String[] args) {
  4. SpringApplication.run(DemoApplication.class, args);
  5. }
  6. @Bean
  7. AuthorizationInterceptor authorizationInterceptor() {
  8. return new AuthorizationInterceptor();
  9. }
  10. @Override
  11. public void addInterceptors(InterceptorRegistry registry) {
  12. // 多个拦截器组成一个拦截器链
  13. // addPathPatterns 用于添加拦截规则
  14. // excludePathPatterns 用于排除拦截
  15. registry.addInterceptor(authorizationInterceptor()).addPathPatterns("/**");
  16. super.addInterceptors(registry);
  17. }
  18. }
  • 处理CurrentUser注解的参数解析器
  1. /**
  2. * @author sukaiyi
  3. */
  4. public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
  5. @Autowired
  6. private UserService userService;
  7. @Override
  8. public boolean supportsParameter(MethodParameter methodParameter) {
  9. //如果参数类型是UserEntity并且有CurrentUser注解则支持
  10. return methodParameter.getParameterType().isAssignableFrom(UserEntity.class) &&
  11. methodParameter.hasParameterAnnotation(CurrentUser.class);
  12. }
  13. @Override
  14. public Object resolveArgument(MethodParameter parameter,
  15. ModelAndViewContainer container,
  16. NativeWebRequest request,
  17. WebDataBinderFactory factory) throws BusinessException {
  18. //取出AuthorizationInterceptor中注入的userId
  19. String currentUserId = (String) request.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST);
  20. if (currentUserId != null) {
  21. return userService.findByUserId(currentUserId);
  22. }
  23. throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");
  24. }
  25. }

参数解析器配置为Bean,并添加

  1. @SpringBootApplication
  2. public class DemoApplication extends WebMvcConfigurerAdapter {
  3. public static void main(String[] args) {
  4. SpringApplication.run(DemoApplication.class, args);
  5. }
  6. @Bean
  7. CurrentUserArgumentResolver currentUserArgumentResolver() {
  8. return new CurrentUserArgumentResolver();
  9. }
  10. @Override
  11. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  12. argumentResolvers.add(currentUserArgumentResolver());
  13. super.addArgumentResolvers(argumentResolvers);
  14. }
  15. }

4. 总结

此时客户端调用Authorization注解修饰的REST接口时,需要在请求头中携带authorization信息,否则将返回401错误,该信息为登陆时服务端返回的token信息。