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/

源碼地址:歡迎關注公眾號:知識追尋者 回復 springSecurity 即可獲取啦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM