用户登录和退出,以及访问请求的token校验
1、功能描述
在项目的实际开发中,肯定会遇到token的生成和校验。一般来说,应该是登录的时候,将登录的请求设置成白名单,登录成功后端生成token返回给前端,然后每次前端发送请求的时候都要带上token,后端会进行token校验,通过校验才可以调用接口,退出登录后token失效
2、代码
2.1、controller层
@RestController
@RequestMapping("/xxx/xxx/xxx/user")
@Api(tags = "用户管理")
@Slf4j
@Validated
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "用户登录")
@WebLog(info = "用户登录")
@SecurityParameter
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody @Validated UserVO userVO) {
Map<String, Object> rm = userService.login(userVO);
boolean flag = (boolean) rm.get("flag");
if (flag) {
return new ResponseEntity<>((String) rm.get("token"), HttpStatus.OK);
} else {
return new ResponseEntity<>((String) rm.get("msg"), HttpStatus.UNAUTHORIZED);
}
}
@ApiOperation(value = "移动端用户登录")
@WebLog(info = "移动端用户登录")
@SecurityParameter
@PostMapping("/mobileLogin")
public ResponseEntity<LoginVO> mobileLogin(@RequestBody @Validated UserVO userVO) {
LoginVO loginVO = new LoginVO();
Map<String, Object> rm = userService.mobileLogin(userVO);
boolean flag = (boolean) rm.get("flag");
if (flag) {
loginVO .setJwt((String) rm.get("token"));
loginVO .setName((String) rm.get("name"));
//为了表示可以传输多个信息,所以这里特地用了对象
return new ResponseEntity<>(loginVO, HttpStatus.OK);
} else {
doctorLoginVO.setJwt((String) rm.get("msg"));
return new ResponseEntity<>(loginVO, HttpStatus.UNAUTHORIZED);
}
}
@ApiOperation(value = "移动端退出登录")
@WebLog(info = "移动端退出登录")
@SecurityParameter
@GetMapping("/mobileLogout")
public ResponseEntity<String> mobileLogout() {
userService.mobileLogout();
return new ResponseEntity<>("已成功退出", HttpStatus.OK);
}
@ApiOperation(value = "退出登录")
@WebLog(info = "退出登录")
@SecurityParameter
@GetMapping("/logout")
public ResponseEntity<String> logout() {
userService.logout();
return new ResponseEntity<>("已成功退出", HttpStatus.OK);
}
}
2.2、service层(此处省略service接口,直接用serviceImpl实现类)
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
//用户登录
@Override
public Map<String, Object> login(UserVO userVO) {
Map<String, Object> rm = new HashMap<>();
rm.put("flag", true);
//authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userVO.getName(), userVO.getPassword());
try {
//此处调用LoginServiceImpl(实现了security接口UserDetailsService的自定义实现类)完成登录的业务拦截
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
//认证未通过
rm.put("flag", false);
rm.put("msg", "登录失败,请检查用户名/密码");
return rm;
}
//认证通过,使用OA账号生成一个jwt
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String name= loginUser.getUser().getName();
rm.put("name", name);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name",name);
//这个标识是为了区分是pc端还是移动端
jsonObject.put("type","0");
String jwt = JwtUtil.createJWT(jsonObject.toString());
//把完整的用户信息存入redis,name作为key
String redisKey = "login:" + name;
redisCache.setCacheObject(redisKey, loginUser);
//设置缓存过期时间,这边随便设置为半个月,15天
redisCache.expire(redisKey, 15 * 60 * 60);
//将jwt返回前端
rm.put("token", jwt);
} catch (BadCredentialsException e) {
rm.put("flag", false);
rm.put("msg", "登录失败,请检查用户名/密码");
log.warn("登录失败:{}", e.getMessage());
}
return rm;
}
//移动端用户登录
@Override
public Map<String, Object> mobileLogin(UserVO userVO) {
Map<String, Object> rm = new HashMap<>();
rm.put("flag", true);
//authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(sysUserVO.getName(), sysUserVO.getPassword());
try {
//此处调用LoginServiceImpl(实现了security接口UserDetailsService的自定义实现类)完成登录的业务拦截
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
//认证未通过
rm.put("flag", false);
rm.put("msg", "登录失败,请检查用户名/密码");
return rm;
}
//认证通过,使用OA账号生成一个jwt
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String oaNumber = loginUser.getUser().getName();
JSONObject jsonObject = new JSONObject();
jsonObject.put("name",name);
jsonObject.put("type","1");
String jwt = JwtUtil.createJWT(jsonObject.toString());
//把完整的用户信息存入redis,name作为key
String redisKey = "mobileLogin:" + name;
redisCache.setCacheObject(redisKey, loginUser);
//设置缓存过期时间,这边随便设置为半个月,15天
redisCache.expire(redisKey, 12 * 60 * 60);
//将jwt返回前端
rm.put("token", jwt);
} catch (BadCredentialsException e) {
rm.put("flag", false);
rm.put("msg", "登录失败,请检查用户名/密码");
log.warn("登录失败:{}", e.getMessage());
}
return rm;
}
//退出登录
@Override
public void logout() {
//从SecurityContextHolder中获取 已登录用户的信息
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//取出name
String name= loginUser.getUser().getName();
//删除redis中指定的值
boolean del = redisCache.deleteObject("login:" + name);
log.info("退出登录:redis中 {} 的用户信息已被删除: {}", name, del);
}
//移动端退出登录
@Override
public void mobileLogout() {
//从SecurityContextHolder中获取 已登录用户的信息
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//取出name
String name= loginUser.getUser().getName();
//删除redis中指定的值
boolean del = redisCache.deleteObject("mobileLogin:" + name);
log.info("退出登录:redis中 {} 的用户信息已被删除: {}", name, del);
}
}
2.3、用户名密码校验
@Slf4j
@Service
public class LoginServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
//这里没有进行复杂的校验,有复杂的逻辑校验都可以加在这里
//我的User对象里面有name和password,在生成对象入库做新增操作前进行了加密
//.setPassword(new BCryptPasswordEncoder().encode(password))
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getName,name);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("登录失败,请检查用户名/密码");
}
//把数据封装成UserDetails返回
return new LoginUser(user);
}
}
2.4、LoginUser
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.5、toekn校验拦截器
/**
* token过滤器,需要将该过滤器添加到Security功能中去(SecurityConfig)
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//登录请求不做处理,直接放行
if ("/xxx/xxx/xxx/login".equals(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
if ("/xxx/xxx/xxx/dmobileLogin".equals(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
//可以设置请求路径中含有xxx就不需要进行token校验
if (request.getRequestURI().contains("/xxx/")) {
filterChain.doFilter(request, response);
return;
}
//从除登录请求外的每个请求中获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行:即使这里因为没有token而放行,也会被后续的过滤器拦截抛出对应的异常,故不在此处拦截
// filterChain.doFilter(request, response);
//检查每个请求都是否携带了Token(除登录请求外)
log.warn("该请求未携带token");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().print("token非法,请登录");
return;
}
//解析token,取出token中的oa账号
JSONObject jsonObject = null;
try {
Claims claims = JwtUtil.parseJWT(token);
String object = claims.getSubject();
jsonObject = JSONObject.parseObject(object);
} catch (Exception e) {
log.warn("token非法:{}", e.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().print("token非法,请登录");
return;
}
String type = jsonObject.getString("type");
String name= jsonObject.getString("name");
if ("0".equals(type)) {
//从redis中获取用户信息对象
String redisKey = "login:" + name;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
//redis中并没有这个用户信息
log.warn("用户未登录");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().print("用户未登录");
return;
}
//!!!把 登录用户的信息 存入SecurityContextHolder!!!
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
//从redis中获取用户信息对象
String redisKey = "mobileLogin:" + name;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
//redis中并没有这个用户信息
log.warn("用户未登录");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().print("用户未登录");
return;
}
//!!!把 登录用户的信息 存入SecurityContextHolder!!!
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
filterChain.doFilter(request, response);
}
}
2.6、SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
//创建BCryptPasswordEncoder注入容器,Security会自动做密码加解密的处理
@Bean
public PasswordEncoder passwordEncoder() {
//就是在这里设置的密码加密方式,我只是以此为例,可以根据需要自行修改
//常用的加密方式有很多,比如AES、RSA、国密加密算法等等
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口 允许匿名访问
.antMatchers("/xxx/xxx/xxx/login").anonymous()
.antMatchers("/doc.html", "/webjars/**", "/v2/**", "/swagger-resources").permitAll()
.anyRequest().permitAll();
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置异常处理器
http.exceptionHandling()
//配置认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint);
// .accessDeniedHandler(accessDeniedHandler);
//允许跨域
http.cors();
}
}