您好,登錄后才能下訂單哦!
這篇文章主要介紹了spring security和jwt整合的方法是什么的相關(guān)知識,內(nèi)容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇spring security和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)機制也是類似于http協(xié)議無狀態(tài)的,它不需要在服務(wù)段保留用戶的認證信息或者鑒權(quán)信息.這就意味著基于token認證機制的用戶就不必考慮在哪一臺服務(wù)器登錄了.
這就為應(yīng)用的擴展提供了遍歷.
認證流程:
這個token必須在每次請求時傳遞給服務(wù)端,它應(yīng)該保存在請求頭里面.另外,服務(wù)器端要支持 CORS(跨來源資源共享策略) ,一般我們在服務(wù)器上這么做就可以了, Access-Control-Allow-Origin: *
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了。
一般是在請求頭里加入Authorization,并加上Bearer標注:如下:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer '
我們之前介紹過,Spring security是基于過濾器(Filter)的,使用過濾器我們可以很容易的攔截某些請求.
因此通過上面對jwt的了解,我們就可以在過濾器中處理token的生成和校驗.
大致流程如下:
1.當用戶進行提交登陸表單時,自定義一個攔截器JWTLoginFilter進行表單參數(shù)的獲取.
2.驗證提交的用戶名密碼是否正確.
3.如果登陸成功,使用jwt頒發(fā)一個token給客戶端,之后的客戶端請求都要帶上這個token.
4.token驗證:再自定義一個過濾器JWTAuthenticationFilter,當用戶訪問需要認證的請求時,攔截該請求,并進行token校驗.
我們?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ù).稍后我們會詳細介紹這個類到底是什么.
/** * @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
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進行驗證了.
/** * 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è)資訊頻道。
免責聲明:本站發(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)容。