SpringSecurity 整合 JWT

項目集成Spring Security(一)javascript

在上一篇基礎上繼續集成 JWT ,實現用戶身份驗證。css

前言

先後端分離項目中,若是直接把 API 接口對外開放,咱們知道這樣風險是很大的,因此在上一篇中咱們引入了 Spring Security ,可是咱們在登錄後缺乏了請求憑證部分。html

什麼是JWT?

JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標準定義的一種能夠安全傳輸的 小巧 和 自包含 的JSON對象。因爲數據是使用數字簽名的,因此是可信任的和安全的。JWT可使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。java

JWT的工做流程

一、用戶進入登陸頁,輸入用戶名、密碼,進行登陸;
二、服務器驗證登陸鑑權,若是改用戶合法,根據用戶的信息和服務器的規則生成 JWT Token
三、服務器將該 token 以 json 形式返回(不必定要json形式,這裏說的是一種常見的作法)
四、用戶獲得 token,存在 localStorage、cookie 或其它數據存儲形式中。之後用戶請求 /protected 中的 API 時,在請求的 header 中加入 Authorization: Bearer xxxx(token)。此處注意token以前有一個7字符長度的 Bearer。
五、服務器端對此 token 進行檢驗,若是合法就解析其中內容,根據其擁有的權限和本身的業務邏輯給出對應的響應結果。
六、用戶取得結果web

以下如所示:算法

7790cc3aade467c985e2e4a8105b89f1.png7790cc3aade467c985e2e4a8105b89f1.png

來看一下 JWT:數據庫

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

token 分紅了三部分,頭部(header),荷載(Payload) 和 簽名(Signature),每部分用 . 分隔,其中頭部和荷載使用了base64編碼,分別解碼以後獲得兩個JSON串:json

第一部分-頭部:後端

{
  "alg""HS256",
  "typ""JWT"
}

alg字段爲加密算法,這是告訴咱們 HMAC 採用 HS512 算法對 JWT 進行的簽名。瀏覽器

第二部分-荷載:

{
  "sub""1234567890",
  "name""John Doe",
  "iat"1516239022
}

荷載的字段及含義:

  • iss: 該JWT的簽發者
  • sub: 該JWT所面向的用戶
  • aud: 接收該JWT的一方
  • exp(expires): 何時過時,這裏是一個Unix時間戳
  • iat(issued at): 在何時簽發的

這段告訴咱們這個Token中含有的數據聲明(Claim),這個例子裏面有三個聲明:sub, name 和 iat。在咱們這個例子中,分別表明着
所面向的用戶、用戶名、建立時間,固然你能夠把任意數據聲明在這裏。

第三部分-簽名:

第三部分簽名則不能使用base64解碼出來,該部分用於驗證頭部和荷載數據的完整性。

JWT的生成和解析

引入依賴:

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

建立一個測試類嘗試一下 JWT 的生成:

public class Test {

    public static void main(String[] args){
        String token = Jwts.builder()
                主題 放入用戶名
                .setSubject("niceyoo")
                自定義屬性 放入用戶擁有請求權限
                .claim("authorities","admin")
                失效時間
                .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
                簽名算法和密鑰
                .signWith(SignatureAlgorithm.HS512, "tmax")
                .compact();
        System.out.println(token);
    }

}

控制檯打印以下:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1ODM1M30.keCiHrcEr0IWXfZLocgHS8znn7uSiaZW1IT6bTs-EQG0NPsb6-Aw_XbGQea4mez2CcAflgMqtzIpsDjZsUOVug

數據聲明(Claim)是一個自定義屬性,能夠用來放入用戶擁有請求權限。上邊爲簡單直接傳了一個 'admin'。

再看看解析:

public static void main(String[] args){

    try {
        解析token
        Claims claims = Jwts.parser()
                .setSigningKey("tmax")
                .parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1OTc2Mn0.MkSJtGaVePLa-eM3gylh1T3fwODg-6ceDDOxscXAQKun-qNrbQFcKPNqXhblbXPNLhaJyEnwugNANCTs98UNmA")
                .getBody();

        System.out.println(claims);
        獲取用戶名
        String username = claims.getSubject();
        System.out.println("username:"+username);
        獲取權限
        String authority = claims.get("authorities").toString();
        System.out.println("權限:"+authority);
    } catch (ExpiredJwtException e) {
        System.out.println("jwt異常");
    } catch (Exception e){
        System.out.println("異常");
    }
}

控制檯打印:

{sub=niceyoo, authorities=admin, exp=1559459762}
username:niceyoo
權限:admin

JWT 自己沒啥難度,但安全總體是一個比較複雜的事情,JWT 只不過提供了一種基於 token 的請求驗證機制。但咱們的用戶權限,對於 API 的權限劃分、資源的權限劃分,用戶的驗證等等都不是JWT負責的。也就是說,請求驗證後,你是否有權限看對應的內容是由你的用戶角色決定的。所接下來纔是咱們的重點,Spring Security 整合 JWT。

集成JWT

要想要 JW T在 Spring 中工做,咱們應該新建一個 JWT filter,並把它配置在 WebSecurityConfig 中。

WebSecurityConfigurerAdapter.java

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailHandler failHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());加密
    }

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

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();

        registry.and()
            表單登陸方式
            .formLogin()
            .permitAll()
            成功處理類
            .successHandler(successHandler)
            失敗
            .failureHandler(failHandler)
            .and()
            .logout()
            .permitAll()
            .and()
            .authorizeRequests()
            任何請求
            .anyRequest()
            須要身份認證
            .authenticated()
            .and()
            關閉跨站請求防禦
            .csrf().disable()
            先後端分離採用JWT 不須要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            添加JWT過濾器 除已配置的其它請求都需通過此過濾器
            .addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));
    }
}

相較於上一篇主要多了以下一行配置:

.addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));

JWTAuthenticationFilter.java

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter   {

    private Integer tokenExpireTime;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, Integer tokenExpireTime) {
        super(authenticationManager);
        this.tokenExpireTime = tokenExpireTime;
    }

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        String header = request.getHeader(SecurityConstant.HEADER);
        if(StrUtil.isBlank(header)){
            header = request.getParameter(SecurityConstant.HEADER);
        }
        Boolean notValid = StrUtil.isBlank(header) || (!header.startsWith(SecurityConstant.TOKEN_SPLIT));
        if (notValid) {
            chain.doFilter(request, response);
            return;
        }
        try {
            UsernamePasswordAuthenticationToken 繼承 AbstractAuthenticationToken 實現 Authentication
            因此當在頁面中輸入用戶名和密碼以後首先會進入到 UsernamePasswordAuthenticationToken驗證(Authentication),
            UsernamePasswordAuthenticationToken authentication = getAuthentication(header, response);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }catch (Exception e){
            e.toString();
        }

        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String header, HttpServletResponse response) {

        用戶名
        String username = null;
        權限
        List<GrantedAuthority> authorities = new ArrayList<>();


        try {
            解析token
            Claims claims = Jwts.parser()
                    .setSigningKey(SecurityConstant.JWT_SIGN_KEY)
                    .parseClaimsJws(header.replace(SecurityConstant.TOKEN_SPLIT, ""))
                    .getBody();
            logger.info("claims:"+claims);
            獲取用戶名
            username = claims.getSubject();
            logger.info("username:"+username);
            獲取權限
            String authority = claims.get(SecurityConstant.AUTHORITIES).toString();
            logger.info("authority:"+authority);
            if(!StringUtils.isEmpty(authority)){
                authorities.add(new SimpleGrantedAuthority(authority));
            }

        } catch (ExpiredJwtException e) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,401,"登陸已失效,請從新登陸"));
        } catch (Exception e){
            log.error(e.toString());
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"解析token錯誤"));
        }

        if(StrUtil.isNotBlank(username)) {
            踩坑提醒 此處password不能爲null
            User principal = new User(username, "", authorities);
            return new UsernamePasswordAuthenticationToken(principal, null, authorities);
        }
        return null;
    }
}

接下來咱們啓動項目看看:

訪問項目中已有的連接:

http://localhost:7777/tmax/videoCategory/getAll

老樣子認證一波:

其中 niceyoo、 爲數據庫用戶信息

登錄成功後獲取返回的 token,注意,此 token 是由 JWT 生成的:

 String token = SecurityConstant.TOKEN_SPLIT + Jwts.builder()
            主題 放入用戶名
            .setSubject(username)
            自定義屬性 放入用戶擁有請求權限
            .claim(SecurityConstant.AUTHORITIES, authorities)
            失效時間
            .setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
            簽名算法和密鑰
            .signWith(SignatureAlgorithm.HS512, SecurityConstant.JWT_SIGN_KEY)
            .compact();

瀏覽器返回 token 以下:

ad45accf0b31c606a10c568acbddec19.pngad45accf0b31c606a10c568acbddec19.png

而後咱們經過 token 憑證去訪問上邊的方法:

e3c05b207e1ec11e1f23560ef1b724b6.pnge3c05b207e1ec11e1f23560ef1b724b6.png

後臺打印信息:

claims:{sub=niceyoo, authorities=admin, exp=1559472866}
username:niceyoo
authority:admin

隨便改一下 token ,返回以下:

172cbf88bbb16a6679931885c2bdd2c4.png172cbf88bbb16a6679931885c2bdd2c4.png
相關文章
相關標籤/搜索