SpringSecurity集成项目实现登录的认证和授权

文章详细描述了如何在SpringBoot应用中使用Redis存储和缓存用户登录凭证,以及利用JWT进行登录认证,同时介绍了如何通过拦截器管理和续期JWTtoken,以提高系统性能和安全性。
摘要由CSDN通过智能技术生成
  1. 用户登录的认证和授权,流程如下:
    在这里插入图片描述
  • 为什么使用Redis储存登录凭证?
    后台在每次处理请求的时候都要查询用户的登录凭证,访问的频率非常高
  • 为什么使用Redis存储用户信息?
    将user缓存到Redis中,获取user时,先从Redis获取。取不到时,则从数据库中查询,再缓存到Redis中。因为很多界面都要用到user信息,并发时,频繁的访问数据库,会导致数据库崩溃。若变更数据库,需要先更新数据库,再清空缓存。
  1. 登录认证的主要实现
//登录认证的service层
@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RoleService roleService;
    @Autowired
    private ResourceService resourceService;
    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public UserVo login(LoginDto loginDto) {
        //认证管理器--认证用户(需准备一个类实现UserDetailsService接口,来从数据库中查询用户信息,重写loadUserByUsername方法)
        UsernamePasswordAuthenticationToken upat =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());
        Authentication authenticate = authenticationManager.authenticate(upat);
        //是否验证成功
        if(!authenticate.isAuthenticated()){
            throw new BaseException("登录失败");
        }

        //获取用户信息
        UserAuth userAuth = (UserAuth) authenticate.getPrincipal();
        //对象拷贝
        UserVo userVo = BeanUtil.toBean(userAuth, UserVo.class);

        //获取资源列表(请求的路径,只有类型为r才是真正的请求按钮,也就是访问路径)
        List<ResourceVo> resourceVoList = resourceService.findResourceVoListByUserId(userVo.getId());
        Set<String> resourcePathsSet = resourceVoList.stream()
                .filter(x->"r".equals(x.getResourceType()))  //资源类型r
                .map(ResourceVo::getRequestPath)
                .collect(Collectors.toSet());
        userVo.setResourceRequestPaths(resourcePathsSet);

        //获取角色列表
        List<RoleVo> roleVoList = roleService.findRoleVoListByUserId(userVo.getId());
        Set<String> roleLabelSet = roleVoList.stream().map(RoleVo::getLabel).collect(Collectors.toSet());
        userVo.setRoleLabels(roleLabelSet);

        //密码设置为空
        userVo.setPassword("");

        Map<String,Object> clamis = new HashMap<>();
        String userVoString = JSONUtil.toJsonStr(userVo);
        clamis.put("currentUser",userVoString);
        //生成token
        String jwtToken = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), clamis);
        //生成uuid
        String uuidToken = UUID.randomUUID().toString();
        userVo.setUserToken(uuidToken);
        //拼接key值  加前缀
        String userTokenKey = UserCacheConstant.USER_TOKEN + userVo.getUsername();
        String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuidToken;
        //设置过期时间
        long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl() / 1000);
        //存储redis   username:uuid
        redisTemplate.opsForValue().set(userTokenKey, uuidToken,ttl, TimeUnit.SECONDS);
        //存储redis  uuid:jwttoken    方便后登录的用户替换旧登录的用户
        redisTemplate.opsForValue().set(jwtTokenKey,jwtToken,ttl,TimeUnit.SECONDS);
        
        //返回vo
        return userVo;
    }
}

然后,配置请求地址

@Configuration
@EnableConfigurationProperties(SecurityConfigProperties.class)
public class SecurityConfig  {
    @Autowired
    JwtAuthorizationManager jwtAuthorizationManager;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //忽略地址
        http.authorizeHttpRequests()
                .antMatchers( "/security/login" )
                .permitAll();
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session
        http.headers().cacheControl().disable();//关闭缓存

        return http.build();
    }
	//任务管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return  authenticationConfiguration.getAuthenticationManager();
    }
    //BCrypt密码编码
    @Bean
    public BCryptPasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  1. 用户数据存入线程中,当用户请求其他业务需要当前用户信息的时候,可以直接获取当前登录人信息。新增拦截器,该流程如下:
    在这里插入图片描述
//拦截器
@Component
public class UserTokenIntercept implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //是否是handle,不是即放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //从头部中拿到当前userToken
        String userToken = request.getHeader(SecurityConstant.USER_TOKEN);
        if (!EmptyUtil.isNullOrEmpty(userToken)) {
            String jwtTokenKey = UserCacheConstant.JWT_TOKEN + userToken;
            //redis中获取
            String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
            if (!EmptyUtil.isNullOrEmpty(jwtToken)) {
            //解析jwt
                Object userObj = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken).get("currentUser");
                String currentUser = String.valueOf(userObj);
                //放入当前线程中:用户当前的web直接获得user使用
                UserThreadLocal.setSubject(currentUser);
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除当前线程中的参数
        UserThreadLocal.removeSubject();
    }
}

然后,配置web的config文件,使这个拦截器生效。

public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    UserTokenIntercept userTokenIntercept;
    //拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //userToken拦截
registry.addInterceptor(userTokenIntercept).excludePathPatterns("无需拦截的路径").addPathPatterns("/**");
  1. 验证登录-自定义授权管理器
@Component
public class JwtAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        //用户当前请求路径  GET/nursing_project/**
        String method = requestAuthorizationContext.getRequest().getMethod();
        String requestURI = requestAuthorizationContext.getRequest().getRequestURI();
        String targetUrl = (method+requestURI);

        //获得请求中的认证后传递过来的userToken
        String userToken = requestAuthorizationContext.getRequest().getHeader(SecurityConstant.USER_TOKEN);
        //如果userToken为空,则当前请求不合法
        if (EmptyUtil.isNullOrEmpty(userToken)){
            return new AuthorizationDecision(false);
        }

        //通过userToken获取jwtToken
        String jwtTokenKey = UserCacheConstant.JWT_TOKEN+userToken;
        //key:uuid
        String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);
        //如果jwtToken为空,则当前请求不合法
        if (EmptyUtil.isNullOrEmpty(jwtToken)){
            return new AuthorizationDecision(false);
        }

        //校验jwtToken是否合法
        Claims cla = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken);
        if (ObjectUtil.isEmpty(cla)) {
            //token失效
            return new AuthorizationDecision(false);
        }

        //如果校验jwtToken通过,则获得userVo对象
        UserVo userVo = JSONObject.parseObject(String.valueOf(cla.get("currentUser")),UserVo.class);

        //用户剔除校验:redis中最新的userToken与前端传入的userToken不符合,则认为当前用户被后续用户剔除
        //key:username  value:uuid
        String currentUserToken = redisTemplate.opsForValue().get(UserCacheConstant.USER_TOKEN + userVo.getUsername());
        if (!userToken.equals(currentUserToken)){
            return new AuthorizationDecision(false);
        }

        //如果当前UserToken存活时间少于10分钟,则进行续期
        Long remainTimeToLive = redisTemplate.opsForValue().getOperations().getExpire(jwtTokenKey);
        if (remainTimeToLive.longValue()<= 600){
            //续期:jwtToken需要重新生成;userToken只需要重新设置过期时间
            //jwt生成的token也会过期,所以需要重新生成jwttoken
            Map<String, Object> claims = new HashMap<>();
            String userVoJsonString = String.valueOf(cla.get("currentUser"));
            claims.put("currentUser", userVoJsonString);

            //jwtToken令牌颁布
            String newJwtToken = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);
            long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl()) / 1000;
            //重新存入
            redisTemplate.opsForValue().set(jwtTokenKey, newJwtToken, ttl, TimeUnit.SECONDS);
            //设置过期时间
            redisTemplate.expire(UserCacheConstant.USER_TOKEN + userVo.getUsername(), ttl, TimeUnit.SECONDS);
        }

        //当前用户资源是否包含当前URL
        for (String resourceRequestPath : userVo.getResourceRequestPaths()) {
            boolean isMatch = antPathMatcher.match(resourceRequestPath, targetUrl);
            if (isMatch){
                return new AuthorizationDecision(true);
            }
        }
        return new AuthorizationDecision(false);
    }
}

然后,修改配置文件中部分代码,来过滤请求,加载授权管理器

//忽略地址
        List<String> ignoreUrl = securityConfigProperties.getIgnoreUrl();//从配置文件中获得地址
        http.authorizeHttpRequests()
                .antMatchers( ignoreUrl.toArray( new String[ignoreUrl.size() ] ) )
                .permitAll()
                .anyRequest().access(jwtAuthorizationManager);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值