springboot?security使用jwt認(rèn)證方式
前言
在前面的幾篇文章中:
- spring boot security快速使用示例
- spring boot security之前后端分離配置
- spring boot security自定義認(rèn)證
- spring boot security驗(yàn)證碼登錄示例
基本對常用的基于cookie和session的認(rèn)證使用場景都已覆蓋。但是session屬于有狀態(tài)認(rèn)證,本文給出一個無狀態(tài)的認(rèn)證:jwt認(rèn)證示例。
代碼示例
下面會提供完整的示例代碼:
依賴
使用的spring boot 2.6.11版本,jdk8。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
定義mapper
定義一個查詢用戶信息的接口:
@Component public class UserMapper { public User select(String username) { return new User(username, "pass"); } }
定義用戶信息的實(shí)體bean
@Data public class User { private String username; private String password; private String captcha; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public User(String username, String password, String captcha) { this.username = username; this.password = password; this.captcha = captcha; } }
security相關(guān)的類
- 實(shí)現(xiàn)spring security內(nèi)置的UserDetailsService接口,根據(jù)用戶名返回用戶信息:
@Slf4j @Component public class UserDetailsServiceImpl implements UserDetailsService { public static final UserDetails INVALID_USER = new org.springframework.security.core.userdetails.User("invalid_user", "invalid_password", Collections.emptyList()); private final UserMapper userMapper; public UserDetailsServiceImpl(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根據(jù)用戶名從數(shù)據(jù)庫查詢用戶信息 User user = userMapper.select(username); if (user == null) { /** * 如果沒查詢到這個用戶,考慮兩種選擇: * 1. 返回一個標(biāo)記無效用戶的常量對象 * 2. 返回一個不可能認(rèn)證通過的用戶 */ return INVALID_USER; // return new User(username, System.currentTimeMillis() + UUID.randomUUID().toString(), Collections.emptyList()); } /** * 這里返回的用戶密碼是否為庫里保存的密碼,是明文/密文,取決于認(rèn)證時密碼比對部分的實(shí)現(xiàn),每個人的場景不一樣, * 因?yàn)槭褂玫氖遣患用艿腜asswordEncoder,所以可以返回明文 */ return new org.springframework.security.core.userdetails.User(username, user.getPassword(), Collections.emptyList()); } }
- 定義jwt工具類
public class JwtUtil { public static final String SECRET = TextCodec.BASE64.encode("secret"); public static final long EXPIRE_SECONDS = 3600L; /** * 從token中解析出用戶名 */ public static String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } /** * 從token中獲取過期時間 */ public static Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } /** * 解析出token聲明. */ public static <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); return claimsResolver.apply(claims); } /** * token是否過期 */ public static Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } /** * 生成token */ public static String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } /** * token是否合法. */ public static Boolean isValidateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private static String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_SECONDS * 1000)) .signWith(SignatureAlgorithm.HS512, SECRET).compact(); } }
- 定義jwt認(rèn)證的過濾器
@Slf4j @Component public class JwtRequestFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; public JwtRequestFilter(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String username = null; String jwtToken = null; if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { username = JwtUtil.getUsernameFromToken(jwtToken); } catch (Exception e) { log.error("獲取token失敗: {}, {}", jwtToken, e.getMessage()); } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 根據(jù)用戶名加載用戶信息 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 判斷token是否有效 if (JwtUtil.isValidateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
- 注冊相關(guān)bean到spring容器
@Configuration public class WebConfiguration { @Bean public PasswordEncoder passwordEncoder() { // 示例,不對密碼進(jìn)行加密處理 return NoOpPasswordEncoder.getInstance(); } @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 設(shè)置加載用戶信息的類 provider.setUserDetailsService(userDetailsService); // 比較用戶密碼的時候,密碼加密方式 provider.setPasswordEncoder(passwordEncoder); return new ProviderManager(Arrays.asList(provider)); } @Bean public Producer defaultKaptcha() { Properties properties = new Properties(); // 還有一些其它屬性,可以進(jìn)行源碼自己看相關(guān)配置,比較清楚了,根據(jù)變量名也能猜出來什么意思了 properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "150"); properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "50"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789abcdefghigklmnopqrstuvwxyz"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
- 自定義 WebSecurityConfigurer
@Component public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { private final JwtRequestFilter jwtRequestFilter; public WebSecurityConfigurer(JwtRequestFilter jwtRequestFilter) { this.jwtRequestFilter = jwtRequestFilter; } @Override protected void configure(HttpSecurity http) throws Exception { // 在這里自定義配置 http.authorizeRequests() // 登錄相關(guān)接口都允許訪問 .antMatchers("/login/**").permitAll() .anyRequest() .authenticated() .and() .exceptionHandling() // 認(rèn)證失敗返回401狀態(tài)碼,前端頁面可以根據(jù)401狀態(tài)碼跳轉(zhuǎn)到登錄頁面 .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase())) .and().cors() // csrf是否決定禁用,請自行考量 .and().csrf().disable() // 采用http 的基本認(rèn)證. .httpBasic() // 設(shè)置session是無關(guān)的 .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
提供登錄接口
@RequestMapping("/login") @RestController public class LoginController { private final AuthenticationManager authenticationManager; private final Producer producer; public LoginController(AuthenticationManager authenticationManager, Producer producer) { this.authenticationManager = authenticationManager; this.producer = producer; } @PostMapping() public Object login(@RequestBody User user, HttpSession session) { Object captcha = session.getAttribute(Constants.KAPTCHA_SESSION_KEY); if (captcha == null || !captcha.toString().equalsIgnoreCase(user.getCaptcha())) { return "captcha is not correct."; } try { // 使用定義的AuthenticationManager進(jìn)行認(rèn)證處理 Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); // 認(rèn)證通過,設(shè)置到當(dāng)前上下文,如果當(dāng)前認(rèn)證過程后續(xù)還有處理的邏輯需要的話。這個示例是沒有必要了 SecurityContextHolder.getContext().setAuthentication(authenticate); String token = JwtUtil.generateToken((UserDetails) authenticate.getPrincipal()); return token; } catch (Exception e) { return "login failed"; } } /** * 獲取驗(yàn)證碼,需要的話,可以提供一個驗(yàn)證碼獲取的接口,在上面的login里把驗(yàn)證碼傳進(jìn)來進(jìn)行比對 */ @GetMapping("/captcha") public void captcha(HttpServletResponse response, HttpSession session) throws IOException { response.setContentType("image/jpeg"); String text = producer.createText(); session.setAttribute(Constants.KAPTCHA_SESSION_KEY, text); BufferedImage image = producer.createImage(text); try (ServletOutputStream out = response.getOutputStream()) { ImageIO.write(image, "jpg", out); } } }
測試
提供一個用于測試的接口
@RequestMapping("/hello") @RestController public class HelloController { @GetMapping("/world") public Object helloWorld() { return "hello, world"; } }
驗(yàn)證
- 獲取驗(yàn)證碼
- 登錄
- 使用登錄的token訪問接口
- 如果沒有token或不正確是訪問受限的
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot @PostConstruct原理用法解析
這篇文章主要介紹了SpringBoot @PostConstruct原理用法解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08SpringMVC基于注解方式實(shí)現(xiàn)上傳下載
本文主要介紹了SpringMVC基于注解方式實(shí)現(xiàn)上傳下載,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04基于Java實(shí)現(xiàn)圖片相似度對比的示例代碼
很多時候我們需要將兩個圖片進(jìn)行對比,確定兩個圖片的相似度。本文將利用Java和OpenCV庫實(shí)現(xiàn)圖片相似度對比,感興趣的可以動手嘗試一下2022-07-07SpringBoot實(shí)現(xiàn)日志鏈路追蹤的項(xiàng)目實(shí)踐
在分布式系統(tǒng)中,由于請求的處理過程可能會跨越多個服務(wù),因此,對請求的追蹤變得尤為重要,本文主要介紹了SpringBoot實(shí)現(xiàn)日志鏈路追蹤的項(xiàng)目實(shí)踐,感興趣的可以了解一下2024-03-03生產(chǎn)消費(fèi)者模式實(shí)現(xiàn)方式和線程安全問題代碼示例
這篇文章主要介紹了生產(chǎn)消費(fèi)者模式實(shí)現(xiàn)方式和線程安全問題代碼示例,具有一定借鑒價值,需要的朋友可以參考下2017-12-12Java基礎(chǔ)之多線程的三種實(shí)現(xiàn)方式
這篇文章主要介紹了Java基礎(chǔ)之多線程的三種實(shí)現(xiàn)方式,文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java基礎(chǔ)的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04關(guān)于Feign的覆寫默認(rèn)配置和Feign的日志
這篇文章主要介紹了關(guān)于Feign的覆寫默認(rèn)配置和Feign的日志方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06