閱讀本文以前,首先要了解 Spring security 的相關內容,最起碼須要將我以前的系列文章看完。其次須要學習 jwt 的相關知識。關於 jwt(全稱JSON Web Token) ,推薦參考阮一峯大神的 JSON Web Token 入門教程,也可翻看其餘相關資料。html
業務需求:後臺的一些接口須要用戶登陸(好比獲取用戶列表:user/list)時,纔有權限訪問。git
業務實現:客戶端訪問 user/list 接口時,須要在請求頭攜帶 token,而後後臺經過判斷 token 的有效性去選擇放行仍是攔截請求。github
業務難點:token 的有效性如何判斷?web
難點攻破:token的有效性藉助 jwt,因此須要充分理解 jwt 的相關知識。spring
業務流程:當訪問 user/list 接口時,後臺經過一個過濾器攔截,從請求頭中獲取token信息,而後經過 jwt工具類校驗token的有效性。json
我本身是將上一篇文章中的工程複製了一份,而後作了些改動,此次改動的東西比較多(增長和刪除了一些類,增長了一些配置),固然我會盡可能講清楚作了哪些改動,因此你們自行選擇新建工程仍是複製一份。api
較之上一篇的內容,本次只新增了 jwt 的依賴包springboot
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--JWT(Json Web Token)--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
這裏能夠簡單看一下,根據 jwt 相關知識以及看後面的代碼會明白他們的意義。app
jwt: tokenHeader: Authorization # JWT存儲的請求頭 secret: mySecret # JWT加解密使用的密鑰 expiration: 604800 # JWT的超期限時間(60*60*24) tokenHead: Bearer # JWT負載中拿到開頭
只是圖個本身方便,從別處 copy 的工具類,須要你們簡單看下。三個類合在一塊兒的做用就是一個 json 格式的響應數據的工具。代碼放在 common 包下面。ide
從別處拷貝了一個 JwtTokenUtil 類,裏面封裝了一些方法,諸如:生成token,判斷token的有效性等。不用糾結方法是如何實現的,只管拿來用(除非出現問題),方法見名知意。
在 UserController 類裏面定義一個登陸方法,以下所示。
須要調用UserDetailService 的loadUserByUsername方法,若是看過我以前的文章,這裏應該清楚這個方法主要作用戶校驗的。
調用 security 的 api, SecurityContextHolder.getContext().setAuthentication(authentication),能夠理解爲security認證用戶登陸成功
生成 token 信息返回給客戶端,這個 token 是供客戶端調用其餘接口(須要用戶登陸才能調用的接口)時使用的。
/** * 用戶登陸接口 * [@param](https://my.oschina.net/u/2303379) username 用戶名 * [@param](https://my.oschina.net/u/2303379) password 密碼 * [@return](https://my.oschina.net/u/556800) */ @PostMapping("/login") public Object login(@RequestParam("username") String username, @RequestParam("password") String password) { try { // 校驗用戶信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 保存用戶登陸態 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);// 進行到這一步,security 纔會認爲你登錄成功 // 根據用戶信息生成 token String token = jwtTokenUtil.generateToken(userDetails); return CommonResult.success(token);// 到這裏, 從咱們的業務角度來講登錄成功了 } catch (AuthenticationException e) { System.out.println("登陸異常: " + e.getMessage()); return CommonResult.failed("登陸失敗"); }
}
過濾器的功能就是用來攔截請求的 url ,而後校驗 token 的有效性。 SecurityContextHolder.getContext().setAuthentication(authentication); 這段代碼着重說一下,咱們能夠簡單理解爲 SecurityContextHolder.getContext() 爲 Security 提供的一個保存已登陸用戶信息的一個容器,經過調用它的 setAuthentication 以及getAuthentication方法,能夠將用戶信息存入容器和從容器中取出。
/** * JWT登陸受權過濾器 */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 1. 從請求頭中取出 token ,key隨便定義,我這裏定義在了配置文件中 String authHeader = request.getHeader(this.tokenHeader); // 2. 判斷是否爲空,以及是否以 "Bearer " 開頭 ps :固定格式 if (authHeader != null && authHeader.startsWith(this.tokenHead)) { String authToken = authHeader.substring(this.tokenHead.length());// 必須以 "Bearer " 開頭 // 3. 從 token 裏獲取用戶名 String username = jwtTokenUtil.getUserNameFromToken(authToken); // 4. 只要 token 沒過時,就讓用戶保持登陸狀態 jwt 令牌只是輔助登陸, 真正是否登陸要看 authentication 是否有效 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 下面這段代碼就是爲了讓 security 保持用戶登陸狀態, 在UserController 裏你也能夠看到 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 核心代碼 SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
在 WebSecurityConfig 配置類的configure方法中的最後一行加上以下代碼。該方法中的其餘代碼的含義,不懂得能夠參考前幾篇博客。
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();// 必須有, 否則會 403 forbidden /*http.formLogin() .loginPage("/loginPage.html")// 自定義登陸頁 .loginProcessingUrl("/form/login")// 自定義登陸 action, 名字隨便起 .successHandler(successHandler)// 自定義登陸成功處理類 .failureHandler(failureHandler);// 自定義登陸失敗處理類*/ // 訪問 "/form/login", "/loginPage.html" 放行 http.authorizeRequests().antMatchers("/user/userInfo", "/user/login", "/form/login", "/loginPage.html").permitAll() .antMatchers("/hello").hasRole("superadmin") // 只有superadmin 角色的用戶才能訪問 .anyRequest().authenticated(); /*http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler)// 用戶沒有訪問權限處理器 .authenticationEntryPoint(entryPoint);// 用戶沒有登陸處理器*/ // 這裏是新增的代碼 http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); }
至此,Sprng Securiy 系列博客更新完畢。這裏可能須要聲明一下, 個人一系列文章可能沒講 Spring Security 的工做流程,或者底層原理,全程在將如何使用 Spring Security。好比下面這段憑空而來的代碼,只是講了這段代碼的邏輯含義,沒有講清楚爲何這樣用。這些是我之後要努力完成的方向。只是但願幫助到須要的人,也歡迎你們給出意見和批評。
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); }