【Spring Security系列】Spring Security+JWT+Redis实现用户认证登录及登出

        本文将以代码示例介绍基于SpringSecurity结合JWT和Redis实现用户的登录和登出。

  • 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
  • 如想要和博主进行技术栈方面的讨论和交流可私信我。

目录

1. Spring Security 介绍

1.1. 什么是身份认证

1.2. 什么是用户授权

2. 开发环境搭建

2.1. 所用工具版本

2.2. pom依赖

3. 核心代码编写

3.1. 编写Security授权配置主文件

3.2. 重写UsernamePasswordAuthenticationFilter过滤器

 3.3. 编写拦截器拦截请求token

3.4. 重写AuthenticationSuccessHandler处理用户登录成功操作

3.5. 重写AuthenticationFailureHandler处理用户登录失败操作

 3.6. 身份校验失败处理器

 3.7. 编写权限校验处理器

3.8. 重写LogoutHandler处理用户退出操作

 3.9. 重写LogoutSuccessHandler处理用户退出成功操作

3.10 编写JWT工具类

4. 代码测试

4.1. 登录测试

4.2. 退出测试


1. Spring Security 介绍

         Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一 套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等

1.1. 什么是身份认证

        身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。 常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

1.2. 什么是用户授权

       当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。 比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说, 系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

2. 开发环境搭建

2.1. 所用工具版本

依赖版本
Spring Boot2.6.14
java1.8
redis6.2

2.2. pom依赖

1. 引入SpringBoot依赖

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.14</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>

2. 引入Spring Security依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

3. 引入redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.14</version>
</dependency>

4. 引入JWT依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

3. 核心代码编写

3.1. 编写Security授权配置主文件

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("authUserDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    @Autowired
    private SecurOncePerRequestFilter securOncePerRequestFilter;
    @Autowired
    private SecurAuthenticationEntryPoint securAuthenticationEntryPoint;
    @Autowired
    private SecurAccessDeniedHandler securAccessDeniedHandler;

    //登录成功处理器
    @Autowired
    private SecurAuthenticationSuccessHandler securAuthenticationSuccessHandler;
    @Autowired
    private SecurAuthenticationFailureHandler securAuthenticationFailureHandler;

    //退出处理器
    @Autowired
    private SecurLogoutHandler securLogoutHandler;
    @Autowired
    private SecurLogoutSuccessHandler securLogoutSuccessHandler;

    @Autowired
    BCryptPasswordEncoderUtil bCryptPasswordEncoderUtil;


    /**
     * 从容器中取出 AuthenticationManagerBuilder,执行方法里面的逻辑之后,放回容器
     *
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoderUtil);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //第1步:解决跨域问题。cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)
        http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();

        //第2步:让Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().headers().cacheControl();

        //第3步:请求权限配置
        //放行注册API请求,其它任何请求都必须经过身份验证.
        http.authorizeRequests()
                .antMatchers(HttpMethod.POST,"/sys-user/register").permitAll()
                .antMatchers("/v2/api-docs", "/v2/feign-docs",
                        "/swagger-resources/configuration/ui",
                        "/swagger-resources","/swagger-resources/configuration/security",
                        "/swagger-ui.html", "/webjars/**").permitAll()
                .anyRequest().authenticated();

        //第4步:拦截账号、密码。覆盖 UsernamePasswordAuthenticationFilter过滤器
        http.addFilterAt(securUsernamePasswordAuthenticationFilter() , UsernamePasswordAuthenticationFilter.class);

        //第5步:拦截token,并检测。在 UsernamePasswordAuthenticationFilter 之前添加 JwtAuthenticationTokenFilter
        http.addFilterBefore(securOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);

        //第6步:处理异常情况:认证失败和权限不足
        http.exceptionHandling().authenticationEntryPoint(securAuthenticationEntryPoint).accessDeniedHandler(securAccessDeniedHandler);

        //第7步:登录,因为使用前端发送JSON方式进行登录,所以登录模式不设置也是可以的。
        http.formLogin();

        //第8步:退出
        http.logout().addLogoutHandler(securLogoutHandler).logoutSuccessHandler(securLogoutSuccessHandler);

    }
    /**
     * 手动注册账号、密码拦截器
     * @return
     * @throws Exception
     */
    @Bean
    SecurUsernamePasswordAuthenticationFilter securUsernamePasswordAuthenticationFilter() throws Exception {
        SecurUsernamePasswordAuthenticationFilter filter = new SecurUsernamePasswordAuthenticationFilter();
        //成功后处理
        filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler);
        //失败后处理
        filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler);

        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
}

3.2. 重写UsernamePasswordAuthenticationFilter过滤器

public class SecurUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    ISysUserService userService;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {

            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            //取authenticationBean
            Map<String, String> authenticationBean = null;
            //用try with resource,方便自动释放资源
            try (InputStream is = request.getInputStream()) {
                authenticationBean = mapper.readValue(is, Map.class);
            } catch (IOException e) {
                //将异常放到自定义的异常类中
                throw  new SecurAuthenticationException(e.getMessage());
            }
            try {
                if (!authenticationBean.isEmpty()) {
                    //获得账号、密码
                    String username = authenticationBean.get(SPRING_SECURITY_FORM_USERNAME_KEY);
                    String password = authenticationBean.get(SPRING_SECURITY_FORM_PASSWORD_KEY);
                    //检测账号、密码是否存在
                    if (userService.checkLogin(username, password)) {
                        //将账号、密码装入UsernamePasswordAuthenticationToken中
                        authRequest = new UsernamePasswordAuthenticationToken(username, password);
                        setDetails(request,authRequest );
                        return this.getAuthenticationManager().authenticate(authRequest);
                    }
                }
            } catch (Exception e) {
                throw new SecurAuthenticationException(e.getMessage());
            }
            return null;
        } else {
            return this.attemptAuthentication(request, response);
        }
    }
}

 3.3. 编写拦截器拦截请求token

@Component
public class SecurOncePerRequestFilter extends OncePerRequestFilter {

@Qualifier("authUserDetailsServiceImpl")
@Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    private String header = "Authorization";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String headerToken = request.getHeader(header);
        if (!StringUtils.isEmpty(headerToken)) {
            String token = headerToken.replace("Bearer", "").trim();
            boolean check = false;
            try {
                check = this.jwtTokenUtil.isTokenExpired(token);
            } catch (Exception e) {
                new Throwable("令牌已过期,请重新登录。"+e.getMessage());
            }
            if (!check){
                //通过令牌获取用户名称
                String username = jwtTokenUtil.getUsernameFromToken(token);
                //判断用户不为空,且SecurityContextHolder授权信息还是空的
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    //通过用户信息得到UserDetails
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    //验证令牌有效性
                    boolean validata = false;
                    try {
                        validata = jwtTokenUtil.validateToken(token, userDetails);
                    }catch (Exception e) {
                        new Throwable("验证token无效:"+e.getMessage());
                    }
                    if (validata) {
                        // 将用户信息存入 authentication,方便后续校验
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(
                                        userDetails,
                                        null,
                                        userDetails.getAuthorities()
                                );
                        //
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        // 将 authentication 存入 ThreadLocal,方便后续获取用户信息
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        chain.doFilter(request, response);
    }
}

3.4. 重写AuthenticationSuccessHandler处理用户登录成功操作

@Component
@Slf4j
public class  SecurAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler {
    @Autowired
    ISysUserService service;
    @Autowired
    RedisUtils redisUtils;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        //取得账号信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        //用户鉴权
        SecurityContextHolder.getContext().setAuthentication(authentication);
        String token=redisUtils.get(TOKEN_KEY+userDetails.getUsername())==null?"":redisUtils.get(TOKEN_KEY+userDetails.getUsername()).toString();
        if(token =="") {
            System.out.println("初次登录,token还没有,生成新token。。。。。。");
            //如果token为空,则去创建一个新的token
            jwtTokenUtil = new JwtTokenUtil();
            token = jwtTokenUtil.generateToken(userDetails);
            redisUtils.set(TOKEN_KEY+userDetails.getUsername(),token,3600L * 11);
        }
        redisUtils.sSetAndTime(VISIT_USER_KEY,60*60*24,userDetails.getUsername()+ System.currentTimeMillis());
        //加载前端菜单
        Map<String,Object> map = new HashMap<>();
        List<UserMenuVo> menus = null;
        try {
            menus = service.getUserMenus(userDetails.getUsername());
        } catch (Exception e) {
            e.printStackTrace();
            R<Map<String,Object>> data = R.failed("获取用户菜单失败");
            this.WriteJSON(request, response, data);
            return;
        }
        map.put("username",userDetails.getUsername());
        map.put("auth",userDetails.getAuthorities());
        map.put("menus",menus);
        map.put("token",token);
        //装入token
        ResponseStructure data = ResponseStructure.success(map);
        //输出
        this.WriteJSON(request, response, data);

    }
}

上述代码主要功能为完成认证并将token塞入redis,因为JWT设置token的过期时间是12个小时,在redis里面我提前了一点,设置了11个小时过期,最后还获取了用户对应菜单(这块之后文章会补充)一起返回给前端。 

3.5. 重写AuthenticationFailureHandler处理用户登录失败操作

@Component
public class SecurAuthenticationFailureHandler extends JSONAuthentication implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException e) throws IOException, ServletException {

        ResponseStructure data = ResponseStructure.instance(ALL_RETURN_401.getCode(),"登录失败:"+e.getMessage());
        //输出
        this.WriteJSON(request, response, data);
    }
}

 3.6. 身份校验失败处理器

@Component
public class SecurAuthenticationEntryPoint extends JSONAuthentication  implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        ResponseStructure data = ResponseStructure.instance(ALL_RETURN_401.getCode(),"token不可用或已过期");
        this.WriteJSON(request, response, data);
    }
}

 3.7. 编写权限校验处理器

@Component
public class SecurAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {

        ResponseStructure data = ResponseStructure.failed("权限不足:"+accessDeniedException.getMessage());
        this.WriteJSON(request, response, data);
    }
}

3.8. 重写LogoutHandler处理用户退出操作

@Component
public class SecurLogoutHandler extends JSONAuthentication implements LogoutHandler {
    private String header = "Authorization";
    @Override
    public void logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Authentication authentication) {

        String headerToken = request.getHeader(header);
        if (!StringUtils.isEmpty(headerToken)) {
            SecurityContextHolder.clearContext();
        }
    }
}

 3.9. 重写LogoutSuccessHandler处理用户退出成功操作

@Component
public class SecurLogoutSuccessHandler extends JSONAuthentication implements LogoutSuccessHandler {
    @Autowired
    RedisUtils redisUtils;
    @Override
    public void onLogoutSuccess(HttpServletRequest request,
                                HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        ResponseStructure data = ResponseStructure.success("退出成功");
        super.WriteJSON(request,response,data);
    }
}

3.10 编写JWT工具类

/**
 * JWT生成令牌、验证令牌、获取令牌
 */
@Component
public class JwtTokenUtil {
    //私钥
    private static final String SECRET_KEY = "coding";

    // 过期时间 毫秒,设置默认12个小时过期
    private static final long EXPIRATION_TIME = 3600000L * 12;

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, userDetails.getUsername());
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username = null;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            System.out.println("e = " + e.getMessage());
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) throws Exception {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(Claims.ISSUED_AT, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) throws Exception {
        AuthUser user = (AuthUser) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET_KEY).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) throws Exception {
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            new Throwable(e);
        }
        return claims;
    }
}

4. 代码测试

4.1. 登录测试

使用postman测试登录接口/login

4.2. 退出测试

使用postman测试登录接口/logout

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
基于SpringBoot2、MyBatisPlus、Spring Security5.7、JWTRedis的开发框架可以提供以下功能和优势: 1. Spring Boot2是一个轻量级的Java开发框架,能够快速构建Web应用程序和微服务。它提供了自动配置和约定大于配置的设计理念,减少了开发的复杂性。 2. MyBatisPlus是一个在MyBatis基础上进行扩展的ORM框架,提供了更简洁、更便捷的数据库访问方式。它支持代码生成、自动SQL映射、分页查询等功能,能够进一步提高开发效率。 3. Spring Security5.7是一个基于Spring的身份认证和授权框架,可以进行用户认证、角色授权、API权限控制等。它提供了一套完整的解决方案,保护应用程序免受各种安全威胁。 4. JWT(Json Web Token)是一种用于跨网络进行身份验证的开放标准。它使用JSON对象作为令牌,可以在客户端和服务器之间传递信息。JWT具有无状态、可扩展、跨平台等特点,适用于分布式系统和移动应用程序。 5. Redis是一种高性能的键值存储系统,它支持数据持久化、集群模式、发布订阅等功能。在开发过程中,可以使用Redis存储JWT令牌、缓存数据等,提高系统的性能和可扩展性。 综上所述,基于SpringBoot2、MyBatisPlus、Spring Security5.7、JWTRedis的开发框架具有快速开发、高效数据库访问、可靠的安全保护和可扩展的分布式支持等优势。它可以帮助开发者快速构建稳定、安全、高性能的Web应用程序和微服务。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端小肥肠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值