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
*/
@Component
public class RedisTokenService implements TokenService {
@Autowired
RedisTemplate<String, TokenEntity> redisTemplate;
@Override
public 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;
}
@Override
public 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());
}
@Override
public TokenEntity getToken(String authentication) {
// userId 为32位字符串
// userId拼接token得到authentication
// 所以要求authentication长度大于32
if (!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;
}
@Override
public void deleteToken(String userId) {
redisTemplate.delete(userId);
}
}
- 登陆/注销的RESTful接口
@RestController
@RequestMapping("/tokens")
public class TokenController {
@Autowired
private UserRepository userRepository;
@Autowired
private 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注解的拦截器
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private 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,并添加拦截器
@SpringBootApplication
public class DemoApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
AuthorizationInterceptor authorizationInterceptor() {
return new AuthorizationInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 多个拦截器组成一个拦截器链
// addPathPatterns 用于添加拦截规则
// excludePathPatterns 用于排除拦截
registry.addInterceptor(authorizationInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
- 处理CurrentUser注解的参数解析器
/**
* @author sukaiyi
*/
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//如果参数类型是UserEntity并且有CurrentUser注解则支持
return methodParameter.getParameterType().isAssignableFrom(UserEntity.class) &&
methodParameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer container,
NativeWebRequest request,
WebDataBinderFactory factory) throws BusinessException {
//取出AuthorizationInterceptor中注入的userId
String 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,并添加
@SpringBootApplication
public class DemoApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
CurrentUserArgumentResolver currentUserArgumentResolver() {
return new CurrentUserArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserArgumentResolver());
super.addArgumentResolvers(argumentResolvers);
}
}
4. 总结
此时客户端调用Authorization注解修饰的REST接口时,需要在请求头中携带authorization信息,否则将返回401错误,该信息为登陆时服务端返回的token信息。