<dependencies>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
</dependencies>
接下来,创建一个JwtUtil类,用于生成和解析JWT令牌。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public boolean isTokenExpired(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
public boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
创建一个JwtAuthenticationFilter类,用于实现JWT的认证过滤器。进行身份验证之前验证图形验证码。
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil, StringRedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.redisTemplate = redisTemplate;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String captchaId = request.getParameter("captchaId");
String inputCode = request.getParameter("inputCode");
String code = redisTemplate.opsForValue().get("CAPTCHA_" + captchaId);
if (StringUtils.isEmpty(code) || !code.equalsIgnoreCase(inputCode)) {
throw new BadCredentialsException("Invalid captcha.");
}
String username = request.getParameter("username");
String password = request.getParameter("password");
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String token = jwtUtil.generateToken(authResult.getName());
response.setHeader("Authorization", "Bearer " + token);
}
}
接下来,创建一个JwtAuthorizationFilter类,用于实现JWT的授权过滤器。
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
super(authenticationManager);
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = header.substring(7);
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
创建一个SecurityConfig类,用于配置Spring Security。添加新的JwtAuthenticationFilter实例。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
public SecurityConfig(JwtUtil jwtUtil, StringRedisTemplate redisTemplate) {
this.jwtUtil = jwtUtil;
this.redisTemplate = redisTemplate;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("password"))
.authorities("ROLE_USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/captcha/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtUtil, redisTemplate))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtUtil))
.sessionManagement().disable();
}
}
接下来,我们将创建一个CaptchaController类,用于生成图形验证码并将其存储到Redis中。
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping(produces = MediaType.IMAGE_PNG_VALUE)
public ResponseEntity<byte[]> createCaptcha() throws IOException {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
String code = lineCaptcha.getCode();
String captchaId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("CAPTCHA_" + captchaId, code, 5, TimeUnit.MINUTES);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
lineCaptcha.write(outputStream);
byte[] bytes = outputStream.toByteArray();
String base64Image = "data:image/png;base64," + Base64Utils.encodeToString(bytes);
return ResponseEntity.status(HttpStatus.OK).header("Captcha-Id", captchaId).body(base64Image.getBytes());
}
@GetMapping("/validate")
public ResponseEntity<Boolean> validateCaptcha(@RequestParam String captchaId, @RequestParam String inputCode) {
String code = redisTemplate.opsForValue().get("CAPTCHA_" + captchaId);
if (code != null && code.equalsIgnoreCase(inputCode)) {
return ResponseEntity.ok(true);
}
return ResponseEntity.ok(false);
}
}
接下来,我们需要修改JwtAuthenticationFilter类,以便在尝试进行身份验证之前验证图形验证码。
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil, StringRedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.redisTemplate = redisTemplate;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String captchaId = request.getParameter("captchaId");
String inputCode = request.getParameter("inputCode");
String code = redisTemplate.opsForValue().get("CAPTCHA_" + captchaId);
if (StringUtils.isEmpty(code) || !code.equalsIgnoreCase(inputCode)) {
throw new BadCredentialsException("Invalid captcha.");
}
String username = request.getParameter("username");
String password = request.getParameter("password");
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String token = jwtUtil.generateToken(authResult.getName());
response.setHeader("Authorization", "Bearer " + token);
}
}