您好,登錄后才能下訂單哦!
這篇文章主要介紹SpringBoot+SpringSecurity+jwt整合的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
準備
首先用的是SpringBoot,省去寫各種xml的時間。然后把依賴加入一下
<!--安全--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
application.yml加上一點配置信息,后面會用
jwt: secret: secret expiration: 7200000 token: Authorization
可能用到代碼,目錄結構放出來一下
配置
SecurityConfig配置
首先是配置SecurityConfig,代碼如下
@Configuration @EnableWebSecurity// 這個注解必須加,開啟Security @EnableGlobalMethodSecurity(prePostEnabled = true)//保證post之前的注解可以使用 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtUserDetailsService jwtUserDetailsService; @Autowired JwtAuthorizationTokenFilter authenticationTokenFilter; //先來這里認證一下 @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoderBean()); } //攔截在這配 @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/haha").permitAll() .antMatchers("/sysUser/test").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").anonymous() .anyRequest().authenticated() // 剩下所有的驗證都需要驗證 .and() .csrf().disable() // 禁用 Spring Security 自帶的跨域處理 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 定制我們自己的 session 策略:調整為讓 Spring Security 不創建和使用 session http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
ok,下面娓娓道來。首先我們這個配置類繼承了WebSecurityConfigurerAdapter,這里面有三個重要的方法需要我們重寫一下:
configure(HttpSecurity http):這個方法是我們配置攔截的地方,exceptionHandling().authenticationEntryPoint(),這里面主要配置如果沒有憑證,可以進行一些操作,這個后面會看jwtAuthenticationEntryPoint這個里面的代碼。進行下一項配置,為了區分必須加入.and()。authorizeRequests()這個后邊配置那些路徑有需要什么權限,比如我配置的那幾個url都是permitAll(),及不需要權限就可以訪問。值得一提的是antMatchers(HttpMethod.OPTIONS, "/**"),是為了方便后面寫前后端分離的時候前端過來的第一次驗證請求,這樣做,會減少這種請求的時間和資源使用。csrf().disable()是為了防止csdf攻擊的,至于什么是csdf攻擊,請自行百度。
另起一行,以示尊重。sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);因為我們要使用jwt托管安全信息,所以把Session禁止掉。看下SessionCreationPolicy枚舉的幾個參數:
public enum SessionCreationPolicy { ALWAYS,//總是會新建一個Session。 NEVER,//不會新建HttpSession,但是如果有Session存在,就會使用它。 IF_REQUIRED,//如果有要求的話,會新建一個Session。 STATELESS;//這個是我們用的,不會新建,也不會使用一個HttpSession。 private SessionCreationPolicy() { } }
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);這行代碼主要是用于JWT驗證,后面再說。
configure(WebSecurity web):這個方法我代碼中沒有用,這個方法主要用于訪問一些靜態的東西控制。其中ignoring()方法可以讓訪問跳過filter驗證。configureGlobal(AuthenticationManagerBuilder auth):這個方法是主要進行驗證的地方,其中jwtUserDetailsService代碼待會會看,passwordEncoder(passwordEncoderBean())是密碼的一種加密方式。
還有兩個注解:@EnableWebSecurity,這個注解必須加,開啟Security。
@EnableGlobalMethodSecurity(prePostEnabled = true),保證post之前的注解可以使用
以上,我們可以確定了哪些路徑訪問不需要任何權限了,至于哪些路徑需要什么權限接著往下看。
SecurityUserDetails
Security 中也有類似于shiro中主體的概念,就是在內存中存了一個東西,方便程序判斷當前請求的用戶有什么權限,需要實現UserDetails這個接口,所以我寫了這個類,并且繼承了我自己的類SysUser。
public enum SessionCreationPolicy { ALWAYS,//總是會新建一個Session。 NEVER,//不會新建HttpSession,但是如果有Session存在,就會使用它。 IF_REQUIRED,//如果有要求的話,會新建一個Session。 STATELESS;//這個是我們用的,不會新建,也不會使用一個HttpSession。 private SessionCreationPolicy() { } }
authorities就是我們的權限,構造方法中我手動把密碼set進去了,這不合適,包括權限我也是手動傳進去的。這些東西都應該從數據庫搜出來,我現在只是體驗一把Security,角色權限那一套都沒寫,所以說明一下就好了,這個構造方法就是傳進來一個標志(我這里用的是username,或者應該用userId什么的都可以),然后給你一個完整的主體信息,供其他地方使用。ok,next。
JwtUserDetailsService
SecurityConfig配置里面不是有個方法是做真正的認證嘛,或者說從數據庫拿信息,具體那認證信息的方法就是在這個方法里面。
@Service public class JwtUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String user) throws UsernameNotFoundException { System.out.println("JwtUserDetailsService:" + user); List<GrantedAuthority> authorityList = new ArrayList<>(); authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); return new SecurityUserDetails(user,authorityList); } }
繼承了Security提供的UserDetailsService接口,實現loadUserByUsername這個方法,我們這里手動模擬從數據庫搜出來一個叫USER的權限,通過剛才的構造方法,模擬生成當前user的信息,供后面jwt Filter一大堆驗證。至于為什么USER權限要加上“ROLE_”前綴,待會會說。
ok,現在我們知道了怎么配置各種url是否需要權限才能訪問,也知道了哪里可以拿到我們的主體信息,那么繼續。
JwtAuthorizationTokenFilter
千呼萬喚始出來,JWT終于可以上場了。至于怎么生成這個token憑證,待會會說,現在假設前端已經拿到了token憑證,要訪問某個接口了,看看怎么進行jwt業務的攔截吧。
@Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenUtil jwtTokenUtil; private final String tokenHeader; public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, @Value("${jwt.token}") String tokenHeader) { this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.tokenHeader = tokenHeader; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestHeader = request.getHeader(this.tokenHeader); String username = null; String authToken = null; if (requestHeader != null && requestHeader.startsWith("Bearer ")) { authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (ExpiredJwtException e) { } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } }
提前說一下,關于@Value注解參數開頭寫了。
doFilterInternal() 這個方法就是這個過濾器的精髓了。首先從header中獲取憑證authToken,從中挖掘出來我們的username,然后看看上下文中是否有我們以這個username為標識的主體。沒有,ok,去new一個(如果對象也可以new就好了。。。)。然后就是驗證這個authToken 是否在有效期呢啊,驗證token是否對啊等等吧。其實我們剛剛把我們SecurityUserDetails這個對象叫做主體,到這里我才發現有點自做多情了,因為生成Security承認的主體是通過UsernamePasswordAuthenticationToken類似與這種類去實現的,之前之所以叫SecurityUserDetails為主體,只是它存了一些關鍵信息。然后將主體信息————authentication,存入上下文環境,供后面使用。
我的很多工具類代碼都放到了jwtTokenUtil,下面貼一下代碼:
@Component public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -3301605591108950415L; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.token}") private String tokenHeader; private Clock clock = DefaultClock.INSTANCE; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public Boolean validateToken(String token, UserDetails userDetails) { SecurityUserDetails user = (SecurityUserDetails) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token) ); } public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } }
根據注釋你能猜個大概吧,就不再說了,有些東西是jwt方面的東西,今天就不再多說了。
JwtAuthenticationEntryPoint
前面還說了一個發現沒有憑證走一個方法,代碼也貼一下。
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { System.out.println("JwtAuthenticationEntryPoint:"+authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"沒有憑證"); } }
實現AuthenticationEntryPoint這個接口,發現沒有憑證,往response中放些東西。
run code
下面跑一下幾個接口,看看具體是怎么具體訪問某個方法的吧,還有前面一點懸念一并解決。
登錄
先登錄一下,看看怎么生成token扔給前端的吧。
@RestController public class LoginController { @Autowired @Qualifier("jwtUserDetailsService") private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public String login(@RequestBody SysUser sysUser, HttpServletRequest request){ final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return token; } @PostMapping("haha") public String haha(){ UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return "haha:"+userDetails.getUsername()+","+userDetails.getPassword(); } }
我們前面配置中已經把login設置為隨便訪問了,這邊通過jwt生成一個token串,具體方法請看jwtTokenUtil.generateToken,已經寫了。只要知道這里面存了username、加密規則、過期時間就好了。
然后跑下haha接口,發現沒問題,正常打印,說明主體也在上下文中了。
需要權限
然后我們訪問一個需要權限的接口吧。
@RestController @RequestMapping("/sysUser") public class SysUserController { @GetMapping(value = "/test") public String test() { return "Hello Spring Security"; } @PreAuthorize("hasAnyRole('USER')") @PostMapping(value = "/testNeed") public String testNeed() { return "testNeed"; } }
訪問testNeed接口,看到沒,@PreAuthorize("hasAnyRole('USER')")這個說明需要USER權限!我們在剛剛生成SecurityUserDetails這個的時候已經模擬加入了USER權限了,所以可以訪問。現在說說為什么加權限的時候需要加入前綴“ROLE_”.看hasAnyRole源碼:
public final boolean hasAnyRole(String... roles) { return hasAnyAuthorityName(defaultRolePrefix, roles); } private boolean hasAnyAuthorityName(String prefix, String... roles) { Set<String> roleSet = getAuthoritySet(); for (String role : roles) { String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; } private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { if (role == null) { return role; } if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) { return role; } if (role.startsWith(defaultRolePrefix)) { return role; } return defaultRolePrefix + role; } 關鍵是 defaultRolePrefix 看這個類最上面 private String defaultRolePrefix = "ROLE_";
人家源碼這么干的,咱們就這么寫唄,咱也不敢問。其實也有不需要前綴的方式,去看看SecurityExpressionRoot這個類吧,用的方法不一樣,也就是@PreAuthorize里面有另外一個參數。
一個重要的問題
先說結論:Security上下文環境(里面有主體)生命周期只限于一次請求。
我做了一個測試:
把SecurityConfig里面configure(HttpSecurity http)這個方法里面
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
這行代碼注釋掉,不走那個jwt filter。就是不每次都添加上下上下文環境。
然后loginController改成
@RestController public class LoginController { @Autowired @Qualifier("jwtUserDetailsService") private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public String login(@RequestBody SysUser sysUser, HttpServletRequest request){ final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); //添加 start UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); //添加 end return token; } @PostMapping("haha") public String haha(){ UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return "haha:"+userDetails.getUsername()+","+userDetails.getPassword(); } }
然后登陸,然后訪問/haha,崩了,發現userDetails里面沒數據。說明這會上下文環境中我們主體不存在。
為什么會這樣呢?
SecurityContextPersistenceFilter 一次請求,filter鏈結束之后 會清除掉Context里面的東西。所說以,主體數據生命周期是一次請求。
源碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { ...假裝有一堆代碼... try { } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); // Crucial removal of SecurityContextHolder contents - do this before anything // else. SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); } }
關鍵就是finally里面 SecurityContextHolder.clearContext(); 這句話。這才體現了那句,把維護信息的事扔給了客戶端,你不請求,我也不知道你有啥。
以上是“SpringBoot+SpringSecurity+jwt整合的示例分析”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。