溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

spring security和jwt整合的方法是什么

發(fā)布時間:2022-10-18 16:00:49 來源:億速云 閱讀:95 作者:iii 欄目:編程語言

這篇文章主要介紹了spring security和jwt整合的方法是什么的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇spring security和jwt整合的方法是什么文章都會有所收獲,下面我們一起來看看吧。

什么是 JWT

json web token (JWT),是為了在網(wǎng)絡(luò)環(huán)境中傳遞聲明而設(shè)計的一種基于JSON的開放標準(RFC 7519),該token 被設(shè)計為緊湊且安全的.特別使用于分布式站點的登陸(SSO)
場景.JWT一般被用來在服務(wù)提供者和服務(wù)認證者之間傳遞身份信息,以便可以從服務(wù)器獲取資源.也可以增加一些額外的其它業(yè)務(wù)邏輯所必需的聲明信息.
該token可直接被用于認證,也可用于被加密.

基于token的鑒權(quán)機制

基于token的鑒權(quán)機制也是類似于http協(xié)議無狀態(tài)的,它不需要在服務(wù)段保留用戶的認證信息或者鑒權(quán)信息.這就意味著基于token認證機制的用戶就不必考慮在哪一臺服務(wù)器登錄了.
這就為應(yīng)用的擴展提供了遍歷.

認證流程:

這個token必須在每次請求時傳遞給服務(wù)端,它應(yīng)該保存在請求頭里面.另外,服務(wù)器端要支持 CORS(跨來源資源共享策略) ,一般我們在服務(wù)器上這么做就可以了, Access-Control-Allow-Origin: *

jwt的組成

jwt的三個組成部分共同構(gòu)成了一個 簽名信息 signature

**這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串.
然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分。**

注意:secret是保存在服務(wù)器端的,jwt的簽發(fā)生成也是在服務(wù)器端的,secret就是用來進行jwt的簽發(fā)和jwt的驗證,
所以,它就是你服務(wù)端的私鑰,在任何場景都不應(yīng)該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。

如何應(yīng)用

一般是在請求頭里加入Authorization,并加上Bearer標注:如下:

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer '

Spring Security 結(jié)合 jwt

我們之前介紹過,Spring security是基于過濾器(Filter)的,使用過濾器我們可以很容易的攔截某些請求.
因此通過上面對jwt的了解,我們就可以在過濾器中處理token的生成和校驗.

大致流程如下:

  • 1.當用戶進行提交登陸表單時,自定義一個攔截器JWTLoginFilter進行表單參數(shù)的獲取.

  • 2.驗證提交的用戶名密碼是否正確.

  • 3.如果登陸成功,使用jwt頒發(fā)一個token給客戶端,之后的客戶端請求都要帶上這個token.

  • 4.token驗證:再自定義一個過濾器JWTAuthenticationFilter,當用戶訪問需要認證的請求時,攔截該請求,并進行token校驗.

Spring Security 安全相關(guān)配置類

我們?yōu)榱撕喕_發(fā)使用spring boot進行項目的快速搭建.需要引入如下依賴:

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

之后我們創(chuàng)建一個controller進行不同級別的驗證.

/**
 * @author itguang
 * @create
@RestController
public class UserController

    @Autowired
    private UserRepository applicationUserRepository;


    @RequestMapping("/hello")
    public String hello(){

        return "hello";
    }

    @RequestMapping("/userList")
    public Map<String, Object> userList(){
        List<User> myUsers = applicationUserRepository.findAll();
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("users",myUsers);
        return map;
    }

    @RequestMapping("/admin")
    public String admin(){

        return "admin";
    }



}

接下來就是配置我們的安全管理類 SecurityConfig :

/**
 * @author itguang
 * @create
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;


    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {

       // auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        // 使用自定義身份驗證組件
        auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService,bCryptPasswordEncoder));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用 csrf
        http.cors().and().csrf().disable().authorizeRequests()
                //允許以下請求
                .antMatchers("/hello").permitAll()
                // 所有請求需要身份認證
                .anyRequest().authenticated()
                .and()
                //驗證登陸
                .addFilter(new JWTLoginFilter(authenticationManager()))
                //驗證token
                .addFilter(new

可以看到我們的Security繼承了 WebSecurityConfigurerAdapter ,關(guān)于WebSecurityConfigurerAdapter我們之前的文章已經(jīng)介紹過,
我們重點關(guān)注的是重載的兩個 configure() 方法.

configure(HttpSecurity http): 這個方法配置了對請求的攔截配置,在這里我們又添加了兩個自定義的過濾器,JWTLoginFilter 和JWTAuthenticationFilter,
分別負責登錄時用戶名密碼的驗證,和攔截請求時對token的驗證.

configure(AuthenticationManagerBuilder auth): 這個方法有點奇怪,我們并沒有使用之前介紹幾種的用戶存儲,而是使用了一個authenticationProvider()
方法,并傳入了一個我們自定義的 AuthenticationProvider 類型的對象作為參數(shù).稍后我們會詳細介紹這個類到底是什么.

登陸信息 驗證過濾器: JWTLoginFilter

/**
 * @author itguang
 * @create
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter


    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }


    /**
     * 接收并解析用戶登陸信息  /login,
     *為已驗證的用戶返回一個已填充的身份驗證令牌,表示成功的身份驗證
     *返回null,表明身份驗證過程仍在進行中。在返回之前,實現(xiàn)應(yīng)該執(zhí)行完成該過程所需的任何額外工作。
     *如果身份驗證過程失敗,就拋出一個AuthenticationException
     *
     *
     * @param request  從中提取參數(shù)并執(zhí)行身份驗證
     * @param response 如果實現(xiàn)必須作為多級身份驗證過程的一部分(比如OpenID)進行重定向,則可能需要響應(yīng)
     * @return 身份驗證的用戶令牌,如果身份驗證不完整,則為null。
     * @throws
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {


        //得到用戶登陸信息,并封裝到 Authentication 中,供自定義用戶組件使用.
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();


        ArrayList<GrantedAuthorityImpl> authorities = new ArrayList<>();

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password, authorities);

        //authenticate()接受一個token參數(shù),返回一個完全經(jīng)過身份驗證的對象,包括證書.
        // 這里并沒有對用戶名密碼進行驗證,而是使用 AuthenticationProvider 提供的 authenticate 方法返回一個完全經(jīng)過身份驗證的對象,包括證書.
//        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

//UsernamePasswordAuthenticationToken 是 Authentication 的實現(xiàn)類
        return authenticationToken;
    }


    /**
     * 登陸成功后,此方法會被調(diào)用,因此我們可以在次方法中生成token,并返回給客戶端
     *
     * @param request
     * @param response
     * @param chain
     * @param
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) {

        String token = Jwts.builder()
                .setSubject(authResult.getName())
                //有效期兩小時
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 2 * 1000))
                //采用什么算法是可以自己選擇的,不一定非要采用HS512
                .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
                .compact();

        response.addHeader("token", "Bearer "

我們可以看到 JWTLoginFilter 繼承了 UsernamePasswordAuthenticationFilter,
并且重寫了它的 attemptAuthentication() 方法和 successfulAuthentication() 方法.

在 attemptAuthentication()方法中,我們就可以得到 /login 提交的用戶名和密碼信息,但這里我們并沒有返回一個認證后的 Authentication,
這是為什么呢?原因就在于,我們在 SecurityConfigure 的方法中,使用了一個自定義的 AuthenticationProvider 實現(xiàn)類,如:

@Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {

       // auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        // 使用自定義身份驗證組件
        auth.authenticationProvider(new

那么 AuthenticationProvider 用來干嘛的呢? 查看他的源碼可以發(fā)現(xiàn):

public interface AuthenticationProvider

         /**
         * 驗證登錄信息,若登陸成功,設(shè)置 Authentication
         *
         * @param authentication
         * @return 一個完全經(jīng)過身份驗證的對象,包括憑證。
         *           如果AuthenticationProvider無法支持已通過的身份驗證對象的身份驗證,則可能返回null。
         *          在這種情況下,將會嘗試支持下一個身份驗證類的驗證提供者。
         * @throws  

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    /**
         * 是否可以提供輸入類型的認證服務(wù)
         *
         * 如果這個AuthenticationProvider支持指定的身份驗證對象,那么返回true。
         * 返回true并不能保證身份驗證提供者能夠?qū)ι矸蒡炞C類的實例進行身份驗證。
         * 它只是表明它可以支持對它進行更深入的評估。身份驗證提供者仍然可以從身份驗證(身份驗證)方法返回null,
         * 以表明應(yīng)該嘗試另一個身份驗證提供者。在運行時管理器的運行時,可以選擇具有執(zhí)行身份驗證的身份驗證提供者。
         *
         * @param authentication
         * @return
    boolean

CustomAuthenticationProvider

AuthenticationProvider(身份驗證提供者) 顧名思義,可以提供一個 Authentication 供Spring Security的上下文使用.

通過 supports 方法我們對特定的 Authentication進行認證,如果返回 true,就交給 authenticate(Authentication authentication) 方法,
此方法一個完全經(jīng)過身份驗證的對象,包括憑證。

如下我們自定義的 CustomAuthenticationProvider:

/**
 * AuthenticationProvider(身份驗證提供者) 顧名思義,可以提供一個 Authentication 供Spring Security的上下文使用,
 *
 * @author itguang
 * @create
public class CustomAuthenticationProvider implements AuthenticationProvider

    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }


    /**
     * 是否可以提供輸入類型的認證服務(wù)
     * <p>
     * 如果這個AuthenticationProvider支持指定的身份驗證對象,那么返回true。
     * 返回true并不能保證身份驗證提供者能夠?qū)ι矸蒡炞C類的實例進行身份驗證。
     * 它只是表明它可以支持對它進行更深入的評估。身份驗證提供者仍然可以從身份驗證(身份驗證)方法返回null,
     * 以表明應(yīng)該嘗試另一個身份驗證提供者。在運行時管理器的運行時,可以選擇具有執(zhí)行身份驗證的身份驗證提供者。
     *
     * @param authentication
     * @return
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }


    /**
     * 驗證登錄信息,若登陸成功,設(shè)置 Authentication
     *
     * @param authentication
     * @return 一個完全經(jīng)過身份驗證的對象,包括憑證。
     * 如果AuthenticationProvider無法支持已通過的身份驗證對象的身份驗證,則可能返回null。
     * 在這種情況下,將會嘗試支持下一個身份驗證類的驗證提供者。
     * @throws
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 獲取認證的用戶名 & 密碼
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        //通過用戶名從數(shù)據(jù)庫中查詢該用戶
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);


        //判斷密碼(這里是md5加密方式)是否正確
        String dbPassword = userDetails.getPassword();
        String encoderPassword = DigestUtils.md5DigestAsHex(password.getBytes());

        if (!dbPassword.equals(encoderPassword)) {
            throw new UsernameIsExitedException("密碼錯誤");
        }


        // 還可以從數(shù)據(jù)庫中查出該用戶所擁有的權(quán)限,設(shè)置到 authorities 中去,這里模擬數(shù)據(jù)庫查詢.
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new GrantedAuthorityImpl("ADMIN"));

        Authentication auth = new UsernamePasswordAuthenticationToken(username, password, authorities);

        return

可見我們在這個 AuthenticationProvider 中對 UsernamePasswordAuthenticationToken 進行認證,

在 authenticate(Authentication authentication)方法中, authentication 就是 我們之前返回的 UsernamePasswordAuthenticationToken,我們可以得到登陸的用戶名和密碼,進行真正的認證.

如果認證成功 就給改 UsernamePasswordAuthenticationToken 設(shè)置對應(yīng)的權(quán)限,最后把已經(jīng)認證的 UsernamePasswordAuthenticationToken 返回即可.

還有我們在通過用戶名從數(shù)據(jù)庫查找用戶時,返回了一個 UserDetails 對象,關(guān)于UserdDetails對象,我們之前的文章已經(jīng)介紹過,不懂得可以去查看一下.

最后,當 CustomAuthenticationProvider 認證成功之后,JWTLoginFilter 中的 successfulAuthentication() 方法機會執(zhí)行,因此我們就可以在這里設(shè)置token了,如下:

/**
     * 登陸成功后,此方法會被調(diào)用,因此我們可以在次方法中生成token,并返回給客戶端
     *
     * @param request
     * @param response
     * @param chain
     * @param
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) {

        String token = Jwts.builder()
                .setSubject(authResult.getName())
                //有效期兩小時
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 2 * 1000))
                //采用什么算法是可以自己選擇的,不一定非要采用HS512
                .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
                .compact();

        response.addHeader("token", "Bearer "

我們使用JWT構(gòu)造了一個token字符串,并把它放在了http請求頭中返回給了客戶端.

至此我們的登陸認證并返回 token就已經(jīng)完成了,接下來就是客戶端攜帶這已經(jīng)獲得token訪問需要認證的資源時,我們需要對改token進行驗證了.

JWTAuthenticationFilter

/**
 * token校驗
 *
 * @author itguang
 * @create
public class JWTAuthenticationFilter extends BasicAuthenticationFilter

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }


    /**
     * 在此方法中檢驗客戶端請求頭中的token,
     * 如果存在并合法,就把token中的信息封裝到 Authentication 類型的對象中,
     * 最后使用  SecurityContextHolder.getContext().setAuthentication(authentication); 改變或刪除當前已經(jīng)驗證的 pricipal
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        String token = request.getHeader("token");

        //判斷是否有token
        if (token == null || !token.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        chain.doFilter(request, response);


    }

    /**
     * 解析token中的信息,并判斷是否過期
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {


        Claims claims = Jwts.parser().setSigningKey("MyJwtSecret")
                .parseClaimsJws(token.replace("Bearer ", ""))
                .getBody();

        //得到用戶名
        String username = claims.getSubject();

        //得到過期時間
        Date expiration = claims.getExpiration();

        //判斷是否過期
        Date now = new Date();

        if (now.getTime() > expiration.getTime()) {

            throw new UsernameIsExitedException("該賬號已過期,請重新登陸");
        }


        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
        }
        return null;
    }


}

由此可以看到 JWTAuthenticationFilter 繼承了 BasicAuthenticationFilter,

BasicAuthenticationFilter 用來處理一個HTTP請求的基本授權(quán)標頭,將結(jié)果放入安全上下文。
總之,這個過濾器負責處理任何具有HTTP請求頭的請求的請求,以及一個基本的身份驗證方案和一個base64編碼的用戶名:密碼令牌。
如果身份驗證成功,那么最終的身份驗證對象將被放入安全上下文。

因此我們就可以繼承 BasicAuthenticationFilter 并重寫 doFilterInternal()方法,在該方法中進行token的驗證,如果驗證成功,將結(jié)果放入安全上下文,如:

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

大功告成

到此,我們就使用Spring Security + JWT ,搭建了一個安全的 resultful api ,接下來我們就進行簡單的測試,這里我是用postman,這是一個非常好用的 http 調(diào)試工具.
我們現(xiàn)在數(shù)據(jù)庫的users表中插入一條用戶信息,用戶名:itguang 密碼: 123456,

接下來,打開post滿,訪問 localhost/login?username=itguang&password=123456

如下:

我們可以看到響應(yīng)頭中多了一個token
properties
token →Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpdGd1YW5nIiwiZXhwIjoxNTE0OTU2NjI3fQ.PIiH7dRrVgPc88kOPtGzvrqZf5l87FRe3h7s9YZVb2zkL_XwRc_v3uhn23bmKqu7G0pSZngdnX0rh_kT1YDwww

這就是我們使用jwt生成的token,現(xiàn)在是加密狀態(tài),接下來我們再訪問 localhost/admin ,并把這個token放到 請求頭中,如下:

會看到返回了正確的字符串,但是如果我們不帶該token值呢?

瀏覽器訪問: http://localhost/admin ,會發(fā)現(xiàn)

403,明顯的沒有權(quán)限禁止訪問,這正是我們想要的結(jié)果.

關(guān)于“spring security和jwt整合的方法是什么”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“spring security和jwt整合的方法是什么”知識都有一定的了解,大家如果還想學習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI