前言
在前面的幾篇文章中:
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。文章來源:http://www.zghlxwxcb.cn/news/detail-522988.html
<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
定義一個查詢用戶信息的接口:文章來源地址http://www.zghlxwxcb.cn/news/detail-522988.html
@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或不正確是訪問受限的
到了這里,關(guān)于spring boot security使用jwt認(rèn)證的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!