SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實現(xiàn)
最近項目的登錄驗證部分,采用了 JWT 驗證的方式。并且既然采用了 Spring Boot 框架,驗證和權(quán)限管理這部分,就自然用了 Spring Security。這里記錄一下具體實現(xiàn)。
在項目采用 JWT 方案前,有必要先了解它的特性和適用場景,畢竟軟件工程里,沒有銀彈。只有合適的場景,沒有萬精油的方案。
一言以蔽之,JWT 可以攜帶非敏感信息,并具有不可篡改性??梢酝ㄟ^驗證是否被篡改,以及讀取信息內(nèi)容,完成網(wǎng)絡(luò)認(rèn)證的三個問題:“你是誰”、“你有哪些權(quán)限”、“是不是冒充的”。
為了安全,使用它需要采用 Https 協(xié)議,并且一定要小心防止用于加密的密鑰泄露。
采用 JWT 的認(rèn)證方式下,服務(wù)端并不存儲用戶狀態(tài)信息,有效期內(nèi)無法廢棄,有效期到期后,需要重新創(chuàng)建一個新的來替換。
所以它并不適合做長期狀態(tài)保持,不適合需要用戶踢下線的場景,不適合需要頻繁修改用戶信息的場景。因為要解決這些問題,總是需要額外查詢數(shù)據(jù)庫或者緩存,或者反復(fù)加密解密,強扭的瓜不甜,不如直接使用 Session。不過作為服務(wù)間的短時效切換,還是非常合適的,就比如 OAuth 之類的。
目標(biāo)功能點
通過填寫用戶名和密碼登錄。
- 驗證成功后, 服務(wù)端生成 JWT 認(rèn)證 token, 并返回給客戶端。
- 驗證失敗后返回錯誤信息。
- 客戶端在每次請求中攜帶 JWT 來訪問權(quán)限內(nèi)的接口。
每次請求驗證 token 有效性和權(quán)限,在無有效 token 時拋出 401 未授權(quán)錯誤。
當(dāng)發(fā)現(xiàn)請求帶著的 token 有效期快到了的時候,返回特定狀態(tài)碼,重新請求一個新 token。
準(zhǔn)備工作
引入 Maven 依賴
針對這個登錄驗證的實現(xiàn),需要引入 Spring Security、jackson、java-jwt 三個包。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.12.1</version> </dependency>
配置 DAO 數(shù)據(jù)層
要驗證用戶前,自然是先要創(chuàng)建用戶實體對象,以及獲取用戶的服務(wù)類。不同的是,這兩個類需要實現(xiàn) Spring Security 的接口,以便將它們集成到驗證框架中。
User
用戶實體類需要實現(xiàn) ”UserDetails“ 接口,這個接口要求實現(xiàn) getUsername、getPassword、getAuthorities 三個方法,用以獲取用戶名、密碼和權(quán)限。以及 isAccountNonExpired```isAccountNonLocked、isCredentialsNonExpired、isEnabled 這四個判斷是否是有效用戶的方法,因為和驗證無關(guān),所以先都返回 true。這里圖方便,用了 lombok。
@Data
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
...
}
UserService
用戶服務(wù)類需要實現(xiàn) “UserDetailsService” 接口,這個接口非常簡單,只需要實現(xiàn) loadUserByUsername(String username) 這么一個方法。這里使用了 MyBatis 來連接數(shù)據(jù)庫獲取用戶信息。
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
@Transactional
public User loadUserByUsername(String username) {
return userMapper.getByUsername(username);
}
...
}
創(chuàng)建 JWT 工具類
這個工具類主要負(fù)責(zé) token 的生成,驗證,從中取值。
@Component
public class JwtTokenProvider {
private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 五分鐘過期
public static final String TOKEN_PREFIX = "Bearer "; // token 的開頭字符串
private String jwtSecret = "XXX 密鑰,打死也不能告訴別人";
...
}
生成 JWT:從以通過驗證的認(rèn)證對象中,獲取用戶信息,然后用指定加密方式,以及過期時間生成 token。這里簡單的只加了用戶名這一個信息到 token 中:
public String generateToken(Authentication authentication) {
User userPrincipal = (User) authentication.getPrincipal(); // 獲取用戶對象
Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); // 設(shè)置過期時間
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 指定加密方式
return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername())
.sign(algorithm); // 簽發(fā) JWT
} catch (JWTCreationException jwtCreationException) {
return null;
}
}
驗證 JWT:指定和簽發(fā)相同的加密方式,驗證這個 token 是否是本服務(wù)器簽發(fā),是否篡改,或者已過期。
public boolean validateToken(String authToken) {
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 和簽發(fā)保持一致
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(authToken);
return true;
} catch (JWTVerificationException jwtVerificationException) {
return false;
}
}
獲取荷載信息:從 token 的荷載部分里解析用戶名信息,這部分是 md5 編碼的,屬于公開信息。
public String getUsernameFromJWT(String authToken) {
try {
DecodedJWT jwt = JWT.decode(authToken);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
return null;
}
}
登錄
登錄部分需要創(chuàng)建三個文件:負(fù)責(zé)登錄接口處理的攔截器,登陸成功或者失敗的處理類。
LoginFilter
Spring Security 默認(rèn)自帶表單登錄,負(fù)責(zé)處理這個登錄驗證過程的過濾器叫“UsernamePasswordAuthenticationFilter”,不過它只支持表單傳值,這里用自定義的類繼承它,使其能夠支持 JSON 傳值,負(fù)責(zé)登錄驗證接口。
這個攔截器只需要負(fù)責(zé)從請求中取值即可,驗證工作 Spring Security 會幫我們處理好。
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("登錄接口方法不支持: " + request.getMethod());
}
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}
LoginSuccessHandler
負(fù)責(zé)在登錄成功后,生成 JWT 給前端。
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
String token = jwtTokenProvider.generateToken(authentication);
responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
response.setContentType("application/json;charset=utf-8");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), responseData);
}
}
LoginFailureHandler
驗證失敗后,返回錯誤信息。
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
ResponseData respBean = setResponseData(exception);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), respBean);
}
private ResponseData setResponseData(AuthenticationException exception) {
if (exception instanceof LockedException) {
return ResponseData.build("用戶已被鎖定");
} else if (exception instanceof CredentialsExpiredException) {
return ResponseData.build("密碼已過期");
} else if (exception instanceof AccountExpiredException) {
return ResponseData.build("用戶名已過期");
} else if (exception instanceof DisabledException) {
return ResponseData.build("賬戶不可用");
} else if (exception instanceof BadCredentialsException) {
return ResponseData.build("驗證失敗");
}
return ResponseData.build("登錄失敗,請聯(lián)系管理員");
}
}
驗證
在成功登陸后,前端在每次發(fā)起請求時攜帶簽發(fā)的 JWT,讓服務(wù)端能識別這是已登錄的用戶。
同時,如果未攜帶 JWT,或攜帶的 token 過期,或者非法,用單獨的處理類返回錯誤信息。
JwtAuthenticationFilter
負(fù)責(zé)在每次請求中,解析請求頭中的 JWT,從中取得用戶信息,生成驗證對象傳遞給下一個過濾器。
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
UsernamePasswordAuthenticationToken authentication = verifyToken(jwt);
if (authentication != null) {
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
}
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
logger.error("無法給 Security 上下文設(shè)置用戶驗證對象", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken == null || !bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) {
logger.info("請求頭不含 JWT token,調(diào)用下個過濾器");
return null;
}
return bearerToken.split(" ")[1].trim();
}
// 驗證token,并生成認(rèn)證后的token
private UsernamePasswordAuthenticationToken verifyToken(String token) {
if (token == null) {
return null;
}
// 認(rèn)證失敗,返回null
if (!jwtProvider.validateToken(token)) {
return null;
}
// 提取用戶名
String username = jwtProvider.getUsernameFromJWT(token);
UserDetails userDetails = new User(username);
// 構(gòu)建認(rèn)證過的token
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
AuthenticationEntryPoint
這個類就比較簡單,只是在驗證不通過后,返回 401 響應(yīng),并記錄錯誤信息。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.error("驗證為通過. 提示信息 - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
集中配置
Spring Security 的功能是通過一系列的過濾器鏈實現(xiàn)的,而配置整個 Spring Security,只需要統(tǒng)一在一個類中配置即可。
現(xiàn)在咱們就創(chuàng)建這個類,繼承自 “WebSecurityConfigurerAdapter”,把上面準(zhǔn)備好的各種文件,一一配置進去。
首先是通過注解,設(shè)置打開全局的 Spring Security 功能,并通過依賴注入,引入剛剛創(chuàng)建的類。
@Configuration
@EnableWebSecurity
public class KanpmSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public LoginFilter loginFilter(LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler)
throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/auth/login");
return loginFilter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
接著,再把用戶獲取服務(wù)類和加密方式,配置到 Spring Security 中去,讓它知道如何去驗證登錄。
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
最后,將JWT過濾器放入過濾器鏈中,用自定義的登錄過濾器替代默認(rèn)的 “UsernamePasswordAuthenticationFilter”,完成功能。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAt(loginFilter(new LoginSuccessHandler(), new LoginFailureHandler()),
UsernamePasswordAuthenticationFilter.class);
}
到此這篇關(guān)于SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity JWT登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity6.0 如何通過JWTtoken進行認(rèn)證授權(quán)
- springSecurity自定義登錄接口和JWT認(rèn)證過濾器的流程
- SpringSecurity+jwt+captcha登錄認(rèn)證授權(quán)流程總結(jié)
- SpringSecurity+Redis+Jwt實現(xiàn)用戶認(rèn)證授權(quán)
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實現(xiàn)
- SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot整合SpringSecurity實現(xiàn)JWT認(rèn)證的項目實踐
- SpringSecurity整合jwt權(quán)限認(rèn)證的全流程講解
- SpringSecurity JWT基于令牌的無狀態(tài)認(rèn)證實現(xiàn)
相關(guān)文章
Java Yml格式轉(zhuǎn)換為Properties問題
本文介紹了作者編寫一個Java工具類來解決在線YAML到Properties轉(zhuǎn)換時屬性內(nèi)容遺漏的問題,通過遍歷YAML文件的樹結(jié)構(gòu),作者成功實現(xiàn)了屬性的完整轉(zhuǎn)換,總結(jié)指出,該工具類適用于多種數(shù)據(jù)類型,并且代碼簡潔易懂2024-12-12
詳解spring boot集成ehcache 2.x 用于hibernate二級緩存
本篇文章主要介紹了詳解spring boot集成ehcache 2.x 用于hibernate二級緩存,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
使用eclipse導(dǎo)入javaWeb項目的圖文教程
這篇文章主要介紹了如何使用eclipse導(dǎo)入別人的javaWeb項目,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07

