1. 使用Token进行身份鉴权
网站应用一般使用Session进行登录用户信息的存储及验证,而在移动端使用Token则更加普遍。它们之间并没有太大区别,Token比较像是一个更加精简的自定义的Session。Session的主要功能是保持会话信息,而Token则只用于登录用户的身份鉴权。所以在移动端使用Token会比使用Session更加简易并且有更高的安全性,同时也更加符合RESTful中无状态的定义。
2. 交互流程
public class TokenEntity {private String userId;private String token;//忽略构造方法和setter/getter}
- User实体
@Entity@Table(name = "user")public class UserEntity {@Id@GeneratedValue(generator = "system-uuid")@GenericGenerator(name = "system-uuid", strategy = "uuid")@Column(name = "user_id", unique = true, nullable = false, length = 32)private String userId;@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")private Date createDate;private String userName;private String password;private boolean dr;//忽略构造方法和setter/getter
- 用于管理Token的服务接口
public interface TokenService {/*** 创建一个token关联上指定用户** @param userId 指定用户的id* @return 生成的token*/TokenEntity createToken(String userId);/*** 检查token是否有效** @param model token* @return 是否有效*/boolean checkToken(TokenEntity model);/*** 从字符串中解析token** @param authentication 加密后的字符串* @return Token实例*/TokenEntity getToken(String authentication);/*** 清除token** @param userId 登录用户的id*/void deleteToken(String userId);}
- 使用Reids管理Token的TokenService 实现类
package com.sukaiyi.demo.certification.service.impl;import com.sukaiyi.demo.certification.constants.Constants;import com.sukaiyi.demo.certification.entity.TokenEntity;import com.sukaiyi.demo.certification.service.TokenService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import java.util.UUID;import java.util.concurrent.TimeUnit;/*** @author sukaiyi*/@Componentpublic class RedisTokenService implements TokenService {@AutowiredRedisTemplate<String, TokenEntity> redisTemplate;@Overridepublic TokenEntity createToken(String userId) {String token = UUID.randomUUID().toString();TokenEntity tokenEntity = new TokenEntity(userId, token);redisTemplate.opsForValue().set(userId, tokenEntity, Constants.TOKEN_EXPIRES_HOUR, TimeUnit.MINUTES);return tokenEntity;}@Overridepublic boolean checkToken(TokenEntity entity) {if (entity == null) {return false;}TokenEntity token = redisTemplate.opsForValue().get(entity.getUserId());if (token == null || StringUtils.isEmpty(token.getToken())) {return false;}return token.getToken().equals(entity.getToken());}@Overridepublic TokenEntity getToken(String authentication) {// userId 为32位字符串// userId拼接token得到authentication// 所以要求authentication长度大于32if (!StringUtils.isEmpty(authentication) && authentication.length() > 32) {TokenEntity tokenEntity = new TokenEntity();String userId = authentication.substring(0, 32);String token = authentication.substring(32);tokenEntity.setUserId(userId);tokenEntity.setToken(token);return tokenEntity;}return null;}@Overridepublic void deleteToken(String userId) {redisTemplate.delete(userId);}}
- 登陆/注销的RESTful接口
@RestController@RequestMapping("/tokens")public class TokenController {@Autowiredprivate UserRepository userRepository;@Autowiredprivate TokenService tokenService;@RequestMapping(method = RequestMethod.POST)public ResponseEntity login(@RequestParam String userName, @RequestParam String password) throws BusinessException {if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {throw new BusinessException(ExceptionCode.NEED_FIELD, "用户名或密码为空");}UserEntity user = userRepository.findUserByUserName(userName);if (user == null) {throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");}if (MD5Util.encode(password).equals(user.getPassword())) {TokenEntity tokenEntity = tokenService.createToken(user.getUserId());return new ResponseEntity<>(ResultEntity.ok(tokenEntity.getUserId() + tokenEntity.getToken()), HttpStatus.OK);}throw new BusinessException(ExceptionCode.AUTHORIZATION_FAILED, "用户名或密码错误");}@Authorization@RequestMapping(method = RequestMethod.DELETE)public ResponseEntity logout(@CurrentUser UserEntity user) {tokenService.deleteToken(user.getUserId());return new ResponseEntity<>(ResultEntity.ok("注销成功"), HttpStatus.OK);}}
其中logout方法的有关的两个注解@Authorization和@CurrentUser:@Authorization标注在controller的rest方法上,表示访问这个资源需要登陆授权;
/*** 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误* @author sukaiyi*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Authorization {}
@CurrentUser标注在方法的参数上,用于自动将当前登陆用户注入到该参数。
/*** 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象** @author sukaiyi*/@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface CurrentUser {}
- 处理Authorization注解的拦截器
@Componentpublic class AuthorizationInterceptor extends HandlerInterceptorAdapter {@Autowiredprivate TokenService tokenService;public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {if (!(handler instanceof HandlerMethod)) {return true;}HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();if (method.getAnnotation(Authorization.class) == null){return true;}String authorization = request.getHeader(Constants.AUTHORIZATION);TokenEntity tokenEntity = tokenService.getToken(authorization);if (tokenService.checkToken(tokenEntity)) {//如果token验证成功,将token对应的用户id存在request中,便于之后注入request.setAttribute(Constants.CURRENT_USER_ID, tokenEntity.getUserId());return true;}else{response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}}}
拦截器配置为Bean,并添加拦截器
@SpringBootApplicationpublic class DemoApplication extends WebMvcConfigurerAdapter {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@BeanAuthorizationInterceptor authorizationInterceptor() {return new AuthorizationInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 多个拦截器组成一个拦截器链// addPathPatterns 用于添加拦截规则// excludePathPatterns 用于排除拦截registry.addInterceptor(authorizationInterceptor()).addPathPatterns("/**");super.addInterceptors(registry);}}
- 处理CurrentUser注解的参数解析器
/*** @author sukaiyi*/public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {@Autowiredprivate UserService userService;@Overridepublic boolean supportsParameter(MethodParameter methodParameter) {//如果参数类型是UserEntity并且有CurrentUser注解则支持return methodParameter.getParameterType().isAssignableFrom(UserEntity.class) &&methodParameter.hasParameterAnnotation(CurrentUser.class);}@Overridepublic Object resolveArgument(MethodParameter parameter,ModelAndViewContainer container,NativeWebRequest request,WebDataBinderFactory factory) throws BusinessException {//取出AuthorizationInterceptor中注入的userIdString currentUserId = (String) request.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST);if (currentUserId != null) {return userService.findByUserId(currentUserId);}throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");}}
参数解析器配置为Bean,并添加
@SpringBootApplicationpublic class DemoApplication extends WebMvcConfigurerAdapter {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}@BeanCurrentUserArgumentResolver currentUserArgumentResolver() {return new CurrentUserArgumentResolver();}@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {argumentResolvers.add(currentUserArgumentResolver());super.addArgumentResolvers(argumentResolvers);}}
4. 总结
此时客户端调用Authorization注解修饰的REST接口时,需要在请求头中携带authorization信息,否则将返回401错误,该信息为登陆时服务端返回的token信息。
