用户登录和退出,以及访问请求的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();
    }

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值