zoukankan      html  css  js  c++  java
  • springSecurity前后端分离集成jwt

    一 前言

    大家好,我是知识追寻者,本篇内容是springSecurity第四篇;没有相关基础的同学请学习后再来看这篇内容;文末附源码地址;

    二 pom

    pom 文件引入的依赖 , security 的启动器支持security 功能;lombok 进行简化开发; fastjson 进行Json处理;

    jjwt 进行jwt token 支持;lang3 字符串处理;

     <dependencies>
         <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.18</version>
                <scope>provided</scope>
            </dependency>
            <!-- fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.62</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.4</version>
            </dependency>
    </dependencies>
    

    三 认证流程

    • SecurityContextHolder,提供SecurityContext的访问权限。
    • SecurityContext,保存Authentication和可能的特定于请求的安全信息。
    • Authentication,以特定于Spring Security的方式代表校验。
    • GrantedAuthority,以反映授予主体的应用程序范围的权限。
    • UserDetails,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。
    • UserDetailsService,在基于String的用户名(或证书ID等)中传递时创建UserDetails

    上面的意思不难理解, 从数据源中获取 用户信息 组装到 UserDetails, 然后通过UserDetailsService,传递 UserDetails; SecurityContextHolder 存储 整个 用户上下文信息,通过SecurityContext 存储 Authentication, 这样就保证了 springSecurity 持有用户信息;

    四 实体

    SysUser 实现 UserDetails 用于储存用户信息, 主要是用户名,密码, 和权限;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Data
    public class SysUser implements UserDetails {
    
        // 用户名
        private String username;
        // 密码
        private String password;
        // 权限信息
        private Set<? extends GrantedAuthority> authorities;
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    

    五 token工具类

    token 工具类主要用于生产 token, 解析token, 校验token;这边需要注意的是,将 权限 归并到了生成 toekn 的步骤,这样通过 token就可以获取 权限,在权限校验时通过token就可以获取权限信息;缺点就进行授权的之后的token应为未更新会造成权限未同步;

    /**
     * @Author lsc
     * <p> </p>
     */
    public class JwtUtil {
    
        private static final String CLAIMS_ROLE = "zszxzRoles";
    
        /**
         * 5天(毫秒)
         */
        private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 5;
        /**
         * JWT密码
         */
        private static final String SECRET = "secret";
    
    
        /**
         * 签发JWT
         */
        public static String getToken(String username, String roles) {
            Map<String, Object> claims = new HashMap<>(8);
            // 主体
            claims.put( CLAIMS_ROLE, roles);
            return Jwts.builder()
                    .setClaims(claims)
                    .claim("username",username)
                    .setExpiration( new Date( Instant.now().toEpochMilli() + EXPIRATION_TIME  ) )// 过期时间
                    .signWith( SignatureAlgorithm.HS512, SECRET )// 加密
                    .compact();
        }
    
        /**
         * 验证JWT
         */
        public static Boolean validateToken(String token) {
            return (!isTokenExpired( token ));
        }
    
        /**
         * 获取token是否过期
         */
        public static Boolean isTokenExpired(String token) {
            Date expiration = getExpireTime( token );
            return expiration.before( new Date() );
        }
    
        /**
         * 根据token获取username
         */
        public static String getUsernameByToken(String token) {
            String username = (String) parseToken( token ).get("username");
            return username;
        }
    
        public static Set<GrantedAuthority> getRolseByToken(String token) {
            String rolse = (String) parseToken( token ).get(CLAIMS_ROLE);
            String[] strArray = StringUtils.strip(rolse, "[]").split(", ");
            Set<GrantedAuthority> authoritiesSet = new HashSet();
            if (strArray.length>0){
                Arrays.stream(strArray).forEach(rols-> {
                    GrantedAuthority authority = new SimpleGrantedAuthority(rols);
                    authoritiesSet.add(authority);
                });
            }
            return authoritiesSet;
        }
    
        /**
         * 获取token的过期时间
         */
        public static Date getExpireTime(String token) {
            Date expiration = parseToken( token ).getExpiration();
            return expiration;
        }
    
        /**
         * 解析JWT
         */
        private static Claims parseToken(String token) {
            Claims claims = Jwts.parser()
                    .setSigningKey( SECRET )
                    .parseClaimsJws( token )
                    .getBody();
            return claims;
        }
    
    }
    

    六 UserDetailsService

    UserDetailsService 用户查询数据库的数据信息,进行用户数据封装到UserDetails, 在进行用户身份认证的时候会走这边; 这边采用官方提供的PasswordEncoder 进行加密; 其配置方式需要在WebSecurityConfig 中 配置;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Component
    @Slf4j
    public class SysUserDetailsService implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        // 登陆验证时,通过username获取用户的所有权限信息; 正式环境中就是查询用户数据授权
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            log.info("------用户{}身份认证-----",username);
            // 新建用户
            SysUser user = new SysUser();
            // 账号
            user.setUsername(username);
            // 密码
            user.setPassword(passwordEncoder.encode("123456"));
            // 设置权限
            Set authoritiesSet = new HashSet();
            // 注意角色权限需要加 ROLE_前缀,否则报403
            GrantedAuthority userPower = new SimpleGrantedAuthority("ROLE_USER");
            GrantedAuthority adminPower = new SimpleGrantedAuthority("ROLE_ADMIN");
            authoritiesSet.add(userPower);
            authoritiesSet.add(adminPower);
            user.setAuthorities(authoritiesSet);
            return user;
        }
    }
    

    七 JWTLoginFilter

    JWTLoginFilter 继承 AbstractAuthenticationProcessingFilter 过滤器;理论上继承 UsernamePasswordAuthenticationFilter 也是 可行,毕竟 UsernamePasswordAuthenticationFilter 是 AbstractAuthenticationProcessingFilter 的实现类;

    JWTLoginFilter 用于用户登陆认证,其实现如下 三个方法 ;

    • attemptAuthentication 用于 尝试认证,如果认证成功会走 successfulAuthentication 方法;如果认证失败会走 unsuccessfulAuthentication 方法;
    • successfulAuthentication 认证成功后我们需要生成一个token,返回以JSON的形式返回给前端;
    • unsuccessfulAuthentication 认证失败,我们通过异常信息判定,然后返回错误信息给前端;
    /**
     * @Author lsc
     * <p> 登陆认证过滤器 </p>
     */
    public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
    
    
        public JWTLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
            super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
            setAuthenticationManager(authenticationManager);
        }
    
    
        /**
         * @Author lsc
         * <p> 登陆认证</p>
         * @Param [request, response]
         * @Return
         */
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
            SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword());
            return getAuthenticationManager().authenticate(authenticationToken);
        }
    
        /**
         * @Author lsc
         * <p> 登陆成功返回token</p>
         * @Param [request, res, chain, auth]
         * @Return
         */
            @Override
            protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain, Authentication auth){
                SysUser principal = (SysUser)auth.getPrincipal();
                String token = JwtUtil.getToken(principal.getUsername(),principal.getAuthorities().toString());
                try {
                    //登录成功時,返回json格式进行提示
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_OK);
                    PrintWriter out = response.getWriter();
                    ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,token);
                    out.write(new ObjectMapper().writeValueAsString(result));
                    out.flush();
                    out.close();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
    
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
            String result="";
            // 账号过期
            if (failed instanceof AccountExpiredException) {
                result="账号过期";
            }
            // 密码错误
            else if (failed instanceof BadCredentialsException) {
                result="密码错误";
            }
            // 密码过期
            else if (failed instanceof CredentialsExpiredException) {
                result="密码过期";
            }
            // 账号不可用
            else if (failed instanceof DisabledException) {
                result="账号不可用";
            }
            //账号锁定
            else if (failed instanceof LockedException) {
                result="账号锁定";
            }
            // 用户不存在
            else if (failed instanceof InternalAuthenticationServiceException) {
                result="用户不存在";
            }
            // 其他错误
            else{
                result="未知异常";
            }
            // 处理编码方式 防止中文乱码
            response.setContentType("text/json;charset=utf-8");
            // 将反馈塞到HttpServletResponse中返回给前台
            response.getWriter().write(JSON.toJSONString(result));
        }
    }
    

    八 WebSecurityConfig

    WebSecurityConfig 是 springSecurity 的配置相关信息;在配置中,可以进行数据访问权限限制,授权异常处理,账号加密方式等配置;

    /**
     * @Author lsc
     * <p> </p>
     */
    @EnableWebSecurity// 开启springSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        DenyHandler denyHandler;
    
        @Autowired
        OutSuccessHandler outSuccessHandler;
    
        @Autowired
        SysUserDetailsService userDetailsService;
    
        @Autowired
        ExpAuthenticationEntryPoint expAuthenticationEntryPoint;
    
        /* *
         * @Author lsc
         * <p> 授权</p>
         * @Param [http]
         */
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()// 授权
                    .antMatchers("/api/download/**").anonymous()// 匿名用户权限
                    .antMatchers("/api/**").hasRole("USER")//普通用户权限
                    .antMatchers("/api/admin/**").hasRole("ADMIN")// 管理员权限
                    .antMatchers("/login").permitAll()
                    //其他的需要授权后访问
                    .anyRequest().authenticated()
                    .and()// 异常
                    .exceptionHandling()
                    .accessDeniedHandler(denyHandler)//授权异常处理
                    .authenticationEntryPoint(expAuthenticationEntryPoint)// 认证异常处理
                    .and()
                    .logout()
                    .logoutSuccessHandler(outSuccessHandler)
                    .and()
                    .addFilterBefore(new JWTLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class)
                    .sessionManagement()
                    // 设置Session的创建策略为:Spring Security不创建HttpSession
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .csrf().disable();// 关闭 csrf 否则post
    
    
        }
    
    
    
        /* *
         * @Author lsc
         * <p>认证 设置加密方式 </p>
         * @Param [auth]
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
    }
    

    九 Handler

    配置中使用到了3个处理类,分别是 denyHandler, outSuccessHandler, expAuthenticationEntryPoint;

    其中 denyHandler 当权限进行校验时,如果权限不足就会走这个处理类

    /**
     * @Author lsc
     * <p> 权限不足处理 </p>
     */
    @Component
    public class DenyHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
            // 设置响应头
            httpServletResponse.setContentType("application/json;charset=utf-8");
            // 返回值
            ResultPage result = ResultPage.error(CodeMsg.PERM_ERROR);
            httpServletResponse.getWriter().write(JSON.toJSONString(result));
        }
    }
    

    outSuccessHandler 是退出登陆处理类,默认地址 localhost:8080/logout;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Component
    public class OutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            // 设置响应头
            httpServletResponse.setContentType("application/json;charset=utf-8");
            // 返回值
            ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,"退出登陆成功");
            httpServletResponse.getWriter().write(JSON.toJSONString(result));
        }
    }
    

    expAuthenticationEntryPoint 负责身份认证通过后异常处理,每个主要身份验证系统都有自己的AuthenticationEntryPoint实现;

    /**
     * @Author lsc
     * <p> </p>
     */
    @Component
    public class ExpAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            // 设置响应头
            httpServletResponse.setContentType("application/json;charset=utf-8");
            // 返回值
            ResultPage result = ResultPage.error(CodeMsg.ACCOUNT_ERROR);
            httpServletResponse.getWriter().write(JSON.toJSONString(result));
        }
    }
    

    十 Controller

    SysUserController 用于 提供权限测试

    /**
     * @Author lsc
     * <p> </p>
     */
    @RestController
    public class SysUserController {
    
    
        @GetMapping("api/admin")
        @PreAuthorize("hasAuthority('ADMIN')")
        public String authAdmin() {
            return "需要ADMIN权限";
        }
    
        @GetMapping("api/test")
        @PreAuthorize("hasAuthority('USER')")
        public String authUser() {
            return "需要USER权限";
        }
    }
    

    整体项目结构如下

    十一 测试

    用户登陆 ,返回token

    请求接口测试,返回数据

    用户退出返回信息;

    最后

    参考文档

    https://blog.csdn.net/Piconjo/article/details/106156383

    https://www.jianshu.com/p/8bd4a6e27e7f

    https://www.jianshu.com/p/bd882078fac4

    https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/

    欢迎关注公众号:知识追寻者

    源码地址:https://github.com/zszxz/study-spring-security

  • 相关阅读:
    树的直径、重心、中心
    DP优化--四边形不等式
    P5569 【SDOI2008】 石子合并(黑科技)
    P3147 262144游戏
    P3205 【HNOI2010】合唱队
    Windows Server 2012 虚拟化实战:网络(一)
    Windows Server 2012 虚拟化实战:存储(二)
    Android使用最小宽度限定符时最小宽度的计算
    Eclipse调试Android App若选择“Use same device for future launches”就再也无法选择其他设备的问题
    Python的模块引用和查找路径
  • 原文地址:https://www.cnblogs.com/zszxz/p/14118801.html
Copyright © 2011-2022 走看看