本篇內(nèi)容主要講解“Spring Boot怎么使用JWT進(jìn)行身份和權(quán)限驗(yàn)證”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“Spring Boot怎么使用JWT進(jìn)行身份和權(quán)限驗(yàn)證”吧!

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個(gè)行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:國際域名空間、虛擬空間、營銷軟件、網(wǎng)站建設(shè)、東興網(wǎng)站維護(hù)、網(wǎng)站推廣。
這個(gè)是 UserControler 主要用來驗(yàn)證權(quán)限配置是否生效。
getAllUser()方法被注解@PreAuthorize("hasAnyRole('ROLE_DEV','ROLE_PM')")修飾代表這個(gè)方法可以被 DEV,PM 這兩個(gè)角色訪問,而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("當(dāng)前訪問該接口的用戶為:" + 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 獲取相關(guān)信息等等方法。
/**
* @author shuang.kou
*/
public class JwtTokenUtils {
/**
* 生成足夠的安全隨機(jī)密鑰,以適合符合規(guī)范的簽名
*/
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();
}
}先來看一下比較重要的兩個(gè)過濾器。
第一個(gè)過濾器主要用于根據(jù)用戶的用戶名和密碼進(jìn)行登錄驗(yàn)證(用戶請求中必須有用戶名和密碼這兩個(gè)參數(shù)),它繼承了 UsernamePasswordAuthenticationFilter 并且重寫了下面三個(gè)方法:
attemptAuthentication(): 驗(yàn)證用戶身份。
successfulAuthentication() : 用戶身份驗(yàn)證成功后調(diào)用的方法。
unsuccessfulAuthentication(): 用戶身份驗(yàn)證失敗后調(diào)用的方法。
/**
* @author shuang.kou
* 如果用戶名和密碼正確,那么過濾器將創(chuàng)建一個(gè)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;
// 設(shè)置登錄請求的 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方法中的源碼是一樣的,
// 只不過由于這個(gè)方法源碼的是把用戶名和密碼這些參數(shù)的名字是死的,所以我們重寫了一下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
loginUser.getUsername(), loginUser.getPassword());
return authenticationManager.authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* 如果驗(yàn)證成功,就生成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());
// 創(chuàng)建 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());
}
}這個(gè)過濾器繼承了 BasicAuthenticationFilter,主要用于處理身份認(rèn)證后才能訪問的資源,它會檢查 HTTP 請求是否存在帶有正確令牌的 Authorization 標(biāo)頭并驗(yàn)證 token 的有效性。
/**
* 過濾器處理所有HTTP請求,并檢查是否存在帶有正確令牌的Authorization標(biāo)頭。例如,如果令牌未過期或簽名密鑰正確。
*
* @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,則進(jìn)行解析,并且設(shè)置授權(quán)信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
super.doFilterInternal(request, response, chain);
}
/**
* 這里從token中獲取用戶信息并新建一個(gè)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;
}
}當(dāng)用戶使用 token 對需要權(quán)限才能訪問的資源進(jìn)行訪問的時(shí)候,這個(gè)類是主要用到的,下面按照步驟來說一說每一步到底都做了什么。
當(dāng)用戶使用系統(tǒng)返回的 token 信息進(jìn)行登錄的時(shí)候 ,會首先經(jīng)過doFilterInternal()方法,這個(gè)方法會從請求的 Header 中取出 token 信息,然后判斷 token 信息是否為空以及 token 信息格式是否正確。
如果請求頭中有 token 并且 token 的格式正確,則進(jìn)行解析并判斷 token 的有效性,然后會在 Spring Security 全局設(shè)置授權(quán)信息SecurityContextHolder.getContext().setAuthentication(getAuthentication(authorization));
我們在講過濾器的時(shí)候說過,當(dāng)認(rèn)證成功的用戶訪問系統(tǒng)的時(shí)候,它的認(rèn)證信息會被設(shè)置在 Spring Security 全局中。那么,既然這樣,我們在其他地方獲取到當(dāng)前登錄用戶的授權(quán)信息也就很簡單了,通過SecurityContextHolder.getContext().getAuthentication();方法即可。為此,我們實(shí)現(xiàn)了一個(gè)專門用來獲取當(dāng)前用戶的類:
/**
* @author shuang.kou
* 獲取當(dāng)前請求的用戶
*/
@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這個(gè)類注入U(xiǎn)serDetailsServiceImpl一致失敗,
* 導(dǎo)致無法正確查找到用戶,所以存入Authentication的Principal為從 token 中取出的當(dāng)前用戶的姓名
*/
private static String getCurrentUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
return (String) authentication.getPrincipal();
}
return null;
}
}JWTAccessDeniedHandler實(shí)現(xiàn)了AccessDeniedHandler主要用來解決認(rèn)證過的用戶訪問需要權(quán)限才能訪問的資源時(shí)的異常。
/**
* @author shuang.kou
* AccessDeineHandler 用來解決認(rèn)證過的用戶訪問需要權(quán)限才能訪問的資源時(shí)的異常
*/
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
/**
* 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而權(quán)限不足的時(shí)候,
* 將調(diào)用此方法發(fā)送401響應(yīng)以及錯誤信息
*/
@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 實(shí)現(xiàn)了 AuthenticationEntryPoint 用來解決匿名用戶訪問需要權(quán)限才能訪問的資源時(shí)的異常
/**
* @author shuang.kou
* AuthenticationEntryPoint 用來解決匿名用戶訪問需要權(quán)限才能訪問的資源時(shí)的異常
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而不提供Token或者Token過期時(shí),
* 將調(diào)用此方法發(fā)送401響應(yīng)以及錯誤信息
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}在 SecurityConfig 配置類中我們主要配置了:
密碼編碼器 BCryptPasswordEncoder(存入數(shù)據(jù)庫的密碼需要被加密)。
為AuthenticationManager 設(shè)置自定義的 UserDetailsService以及密碼編碼器;
在 Spring Security 配置指定了哪些路徑下的資源需要驗(yàn)證了的用戶才能訪問、哪些不需要以及哪些資源只能被特定角色訪問;
將我們自定義的兩個(gè)過濾器添加到 Spring Security 配置中;
將兩個(gè)自定義處理權(quán)限認(rèn)證方面的異常類添加到 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 {
// 設(shè)置自定義的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()
// 指定路徑下的資源需要驗(yàn)證了的用戶才能訪問
.antMatchers("/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
//添加自定義Filter
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session(不創(chuàng)建會話)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授權(quán)異常處理
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.accessDeniedHandler(new JWTAccessDeniedHandler());
}
}跨域:
在這里踩的一個(gè)坑是:如果你沒有設(shè)置exposedHeaders("Authorization")暴露 header 中的"Authorization"屬性給客戶端應(yīng)用程序的話,前端是獲取不到 token 信息的。
@Configuration
public class CorsConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
//暴露header中的其他屬性給客戶端應(yīng)用程序
//如果不設(shè)置這個(gè)屬性前端無法通過response header獲取到Authorization也就是token
.exposedHeaders("Authorization")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}無狀態(tài),服務(wù)器不需要存儲 Session 信息。
有效避免了CSRF 攻擊。
適合移動端應(yīng)用。
單點(diǎn)登錄友好。
注銷登錄等場景下 token 還有效
token 的續(xù)簽問題
到此,相信大家對“Spring Boot怎么使用JWT進(jìn)行身份和權(quán)限驗(yàn)證”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
網(wǎng)站標(biāo)題:SpringBoot怎么使用JWT進(jìn)行身份和權(quán)限驗(yàn)證
網(wǎng)站鏈接:http://chinadenli.net/article14/ipcede.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)建站、網(wǎng)站收錄、微信公眾號、定制開發(fā)、虛擬主機(jī)、建站公司
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)