您好,登錄后才能下訂單哦!
本篇內容主要講解“Spring Boot怎么使用JWT進行身份和權限驗證”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Spring Boot怎么使用JWT進行身份和權限驗證”吧!
這個是 UserControler
主要用來驗證權限配置是否生效。
getAllUser()
方法被注解@PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")
修飾代表這個方法可以被 DEV,PM 這兩個角色訪問,而deleteUserById()
被注解@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
修飾代表只能被 ADMIN 訪問。
/** * @author shuang.kou */ @RestController @RequestMapping("/api") public class UserController { private final UserService userService; private final CurrentUser currentUser; public UserController(UserService userService, CurrentUser currentUser) { this.userService = userService; this.currentUser = currentUser; } @GetMapping("/users") @PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')") public ResponseEntity<Page<User>> getAllUser(@RequestParam(value = "pageNum", defaultValue = "0") int pageNum, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) { System.out.println("當前訪問該接口的用戶為:" + currentUser.getCurrentUser().toString()); Page<User> allUser = userService.getAllUser(pageNum, pageSize); return ResponseEntity.ok().body(allUser); } @DeleteMapping("/user") @PreAuthorize("hasAnyRole('ROLE_ADMIN')") public ResponseEntity<User> deleteUserById(@RequestParam("username") String username) { userService.deleteUserByUserName(username); return ResponseEntity.ok().build(); } }
里面主要有一些常用的方法比如 生成 token 以及解析 token 獲取相關信息等等方法。
/** * @author shuang.kou */ public class JwtTokenUtils { /** * 生成足夠的安全隨機密鑰,以適合符合規范的簽名 */ private static byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); private static SecretKey secretKey = Keys.hmacShaKeyFor(apiKeySecretBytes); public static String createToken(String username, List<String> roles, boolean isRememberMe) { long expiration = isRememberMe ? SecurityConstants.EXPIRATION_REMEMBER : SecurityConstants.EXPIRATION; String tokenPrefix = Jwts.builder() .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) .signWith(secretKey, SignatureAlgorithm.HS256) .claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles)) .setIssuer("SnailClimb") .setIssuedAt(new Date()) .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) .compact(); return SecurityConstants.TOKEN_PREFIX + tokenPrefix; } private boolean isTokenExpired(String token) { Date expiredDate = getTokenBody(token).getExpiration(); return expiredDate.before(new Date()); } public static String getUsernameByToken(String token) { return getTokenBody(token).getSubject(); } /** * 獲取用戶所有角色 */ public static List<SimpleGrantedAuthority> getUserRolesByToken(String token) { String role = (String) getTokenBody(token) .get(SecurityConstants.ROLE_CLAIMS); return Arrays.stream(role.split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } private static Claims getTokenBody(String token) { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); } }
先來看一下比較重要的兩個過濾器。
第一個過濾器主要用于根據用戶的用戶名和密碼進行登錄驗證(用戶請求中必須有用戶名和密碼這兩個參數),它繼承了 UsernamePasswordAuthenticationFilter
并且重寫了下面三個方法:
attemptAuthentication()
: 驗證用戶身份。
successfulAuthentication()
: 用戶身份驗證成功后調用的方法。
unsuccessfulAuthentication()
: 用戶身份驗證失敗后調用的方法。
/** * @author shuang.kou * 如果用戶名和密碼正確,那么過濾器將創建一個JWT Token 并在HTTP Response 的header中返回它,格式:token: "Bearer +具體token值" */ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private ThreadLocal<Boolean> rememberMe = new ThreadLocal<>(); private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; // 設置登錄請求的 URL super.setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { ObjectMapper objectMapper = new ObjectMapper(); try { // 從輸入流中獲取到登錄的信息 LoginUser loginUser = objectMapper.readValue(request.getInputStream(), LoginUser.class); rememberMe.set(loginUser.getRememberMe()); // 這部分和attemptAuthentication方法中的源碼是一樣的, // 只不過由于這個方法源碼的是把用戶名和密碼這些參數的名字是死的,所以我們重寫了一下 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( loginUser.getUsername(), loginUser.getPassword()); return authenticationManager.authenticate(authRequest); } catch (IOException e) { e.printStackTrace(); return null; } } /** * 如果驗證成功,就生成token并返回 */ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { JwtUser jwtUser = (JwtUser) authentication.getPrincipal(); List<String> roles = jwtUser.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); // 創建 Token String token = JwtTokenUtils.createToken(jwtUser.getUsername(), roles, rememberMe.get()); // Http Response Header 中返回 Token response.setHeader(SecurityConstants.TOKEN_HEADER, token); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage()); } }
這個過濾器繼承了 BasicAuthenticationFilter
,主要用于處理身份認證后才能訪問的資源,它會檢查 HTTP 請求是否存在帶有正確令牌的 Authorization 標頭并驗證 token 的有效性。
/** * 過濾器處理所有HTTP請求,并檢查是否存在帶有正確令牌的Authorization標頭。例如,如果令牌未過期或簽名密鑰正確。 * * @author shuang.kou */ public class JWTAuthorizationFilter extends BasicAuthenticationFilter { private static final Logger logger = Logger.getLogger(JWTAuthorizationFilter.class.getName()); public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER); // 如果請求頭中沒有Authorization信息則直接放行了 if (authorization == null || !authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) { chain.doFilter(request, response); return; } // 如果請求頭中有token,則進行解析,并且設置授權信息 SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization)); super.doFilterInternal(request, response, chain); } /** * 這里從token中獲取用戶信息并新建一個token */ private UsernamePasswordAuthenticationToken getAuthentication(String authorization) { String token = authorization.replace(SecurityConstants.TOKEN_PREFIX, ""); try { String username = JwtTokenUtils.getUsernameByToken(token); // 通過 token 獲取用戶具有的角色 List<SimpleGrantedAuthority> userRolesByToken = JwtTokenUtils.getUserRolesByToken(token); if (!StringUtils.isEmpty(username)) { return new UsernamePasswordAuthenticationToken(username, null, userRolesByToken); } } catch (SignatureException | ExpiredJwtException exception) { logger.warning("Request to parse JWT with invalid signature . Detail : " + exception.getMessage()); } return null; } }
當用戶使用 token 對需要權限才能訪問的資源進行訪問的時候,這個類是主要用到的,下面按照步驟來說一說每一步到底都做了什么。
當用戶使用系統返回的 token 信息進行登錄的時候 ,會首先經過doFilterInternal()
方法,這個方法會從請求的 Header 中取出 token 信息,然后判斷 token 信息是否為空以及 token 信息格式是否正確。
如果請求頭中有 token 并且 token 的格式正確,則進行解析并判斷 token 的有效性,然后會在 Spring Security 全局設置授權信息SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
我們在講過濾器的時候說過,當認證成功的用戶訪問系統的時候,它的認證信息會被設置在 Spring Security 全局中。那么,既然這樣,我們在其他地方獲取到當前登錄用戶的授權信息也就很簡單了,通過SecurityContextHolder.getContext().getAuthentication();
方法即可。為此,我們實現了一個專門用來獲取當前用戶的類:
/** * @author shuang.kou * 獲取當前請求的用戶 */ @Component public class CurrentUser { private final UserDetailsServiceImpl userDetailsService; public CurrentUser(UserDetailsServiceImpl userDetailsService) { this.userDetailsService = userDetailsService; } public JwtUser getCurrentUser() { return (JwtUser) userDetailsService.loadUserByUsername(getCurrentUserName()); } /** * TODO:由于在JWTAuthorizationFilter這個類注入UserDetailsServiceImpl一致失敗, * 導致無法正確查找到用戶,所以存入Authentication的Principal為從 token 中取出的當前用戶的姓名 */ private static String getCurrentUserName() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() != null) { return (String) authentication.getPrincipal(); } return null; } }
JWTAccessDeniedHandler
實現了AccessDeniedHandler
主要用來解決認證過的用戶訪問需要權限才能訪問的資源時的異常。
/** * @author shuang.kou * AccessDeineHandler 用來解決認證過的用戶訪問需要權限才能訪問的資源時的異常 */ public class JWTAccessDeniedHandler implements AccessDeniedHandler { /** * 當用戶嘗試訪問需要權限才能的REST資源而權限不足的時候, * 將調用此方法發送401響應以及錯誤信息 */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { accessDeniedException = new AccessDeniedException("Sorry you don not enough permissions to access it!"); response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } }
JWTAuthenticationEntryPoint
實現了 AuthenticationEntryPoint
用來解決匿名用戶訪問需要權限才能訪問的資源時的異常
/** * @author shuang.kou * AuthenticationEntryPoint 用來解決匿名用戶訪問需要權限才能訪問的資源時的異常 */ public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { /** * 當用戶嘗試訪問需要權限才能的REST資源而不提供Token或者Token過期時, * 將調用此方法發送401響應以及錯誤信息 */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); } }
在 SecurityConfig 配置類中我們主要配置了:
密碼編碼器 BCryptPasswordEncoder
(存入數據庫的密碼需要被加密)。
為AuthenticationManager
設置自定義的 UserDetailsService
以及密碼編碼器;
在 Spring Security 配置指定了哪些路徑下的資源需要驗證了的用戶才能訪問、哪些不需要以及哪些資源只能被特定角色訪問;
將我們自定義的兩個過濾器添加到 Spring Security 配置中;
將兩個自定義處理權限認證方面的異常類添加到 Spring Security 配置中;
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsServiceImpl; /** * 密碼編碼器 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService createUserDetailsService() { return userDetailsServiceImpl; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 設置自定義的userDetailsService以及密碼編碼器 auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() // 禁用 CSRF .csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.POST, "/auth/login").permitAll() // 指定路徑下的資源需要驗證了的用戶才能訪問 .antMatchers("/api/**").authenticated() .antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") // 其他都放行了 .anyRequest().permitAll() .and() //添加自定義Filter .addFilter(new JWTAuthenticationFilter(authenticationManager())) .addFilter(new JWTAuthorizationFilter(authenticationManager())) // 不需要session(不創建會話) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 授權異常處理 .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint()) .accessDeniedHandler(new JWTAccessDeniedHandler()); } }
跨域:
在這里踩的一個坑是:如果你沒有設置exposedHeaders("Authorization")
暴露 header 中的"Authorization"屬性給客戶端應用程序的話,前端是獲取不到 token 信息的。
@Configuration public class CorsConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") //暴露header中的其他屬性給客戶端應用程序 //如果不設置這個屬性前端無法通過response header獲取到Authorization也就是token .exposedHeaders("Authorization") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT") .maxAge(3600); } }
無狀態,服務器不需要存儲 Session 信息。
有效避免了CSRF 攻擊。
適合移動端應用。
單點登錄友好。
注銷登錄等場景下 token 還有效
token 的續簽問題
到此,相信大家對“Spring Boot怎么使用JWT進行身份和權限驗證”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。