现在大多都是前后端分离的项目了,系统权限认证是最开始就需要考虑的问题之一,下面是前段时间搓小项目时的spring security+JWT实战应用。
第一步,先将要用到的依赖导入,采用的是spring3.5.7和jjwt0.12.5。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
第二步,新增一个SecurityConfig配置类对Spring Security做定制配置,主要是鉴权策略、session策略、密码加密算法等。
import cn.ghw.web.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
第三步,新增一个JwtTokenService封装token相关功能支持。
import cn.ghw.web.common.SysConstants;
import cn.ghw.web.common.enums.AuthTypeEnum;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Objects;
import java.util.function.Function;
@Slf4j
@Service
public class JwtTokenService {
private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000L; // 30分钟
private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 7天
private static final SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;
private static final byte[] keyBytes = new byte[32];
static {
new SecureRandom().nextBytes(keyBytes);
}
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(keyBytes);
/**
* 生成AccessToken
*/
public String generateAccessToken(Long userId, AuthTypeEnum authType) {
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("userId", userId)
.claim("authType", authType.getCode())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))
.signWith(SECRET_KEY, ALGORITHM)
.compact();
}
/**
* 生成RefreshToken
*/
public String generateRefreshToken(Long userId) {
return Jwts.builder()
.subject(String.valueOf(userId))
.claim(SysConstants.USERID_KEY, userId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))
.signWith(SECRET_KEY, ALGORITHM)
.compact();
}
public boolean validateToken(String token, Long userId) {
Claims claims = getAllClaimsFromToken(token);
return Objects.equals(claims.get("userId", Long.class), userId);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public boolean isTokenExpired(String token) {
try {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (Exception e) {
log.error("检查token是否失效异常:{}", e.getMessage());
return true;
}
}
public Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token).getPayload();
}
// 从Token中获取指定信息
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
}
第四步,新增一个拦截校验token的拦截器类JwtAuthenticationFilter。
import cn.ghw.web.service.JwtTokenService;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenService jwtTokenService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
final String header = request.getHeader("Authorization");
// JWT Token格式: "Bearer token"
if (header != null && header.startsWith("Bearer")) {
try {
Long userId;
String token = header.replace("Bearer ", "");
// 检查token是否失效
if (!jwtTokenService.isTokenExpired(token)) {
Claims claims = jwtTokenService.getAllClaimsFromToken(token);
userId = claims.get("userId", Long.class);
if (userId != null && jwtTokenService.validateToken(token, userId)) {
Map<String, Object> principal = new HashMap<>();
principal.put("userId", userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
principal,
null,
List.of());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
} catch (Exception e) {
log.error("Unable to get JWT Token:{}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
filterChain.doFilter(request, response);
}
}
第五步,实现登录业务和token刷新业务功能即可。
import cn.ghw.web.common.R;
import cn.ghw.web.common.enums.AuthTypeEnum;
import cn.ghw.web.common.exception.BusinessException;
import cn.ghw.web.dto.LoginDto;
import cn.ghw.web.dto.RegisterReq;
import cn.ghw.web.entity.UserEntity;
import cn.ghw.web.service.UserService;
import cn.ghw.web.vo.LoginResp;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class WebAuthController {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<R> login(@RequestBody @Valid LoginDto req) {
try {
// 1.验证账号
String account = req.getAccount();
UserEntity user = userService.queryUserByAccount(account);
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("用户不存在"));
}
// 2.验证密码
String password = req.getPassword();
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("密码错误"));
}
// 3. 认证
LoginResp loginResp = userService.authentication(AuthTypeEnum.PASSWORD, user.getUserId(), account);
if (loginResp == null || StringUtils.isEmpty(loginResp.getAccessToken()) || StringUtils.isEmpty(loginResp.getRefreshToken())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("登录失败"));
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + loginResp.getAccessToken());
headers.setAccessControlExposeHeaders(Collections.singletonList(HttpHeaders.AUTHORIZATION));
return ResponseEntity.ok()
.headers(headers).
body(R.success(loginResp));
} catch (AuthenticationException e) {
log.error("生成token异常:{}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.fail("获取登录失败"));
}
}
@GetMapping("refresh")
public ResponseEntity<String> refreshToken(@RequestParam @NotEmpty(message = "refreshToken不能为空") String refreshToken) {
try {
String accessToken = userService.refreshToken(AuthTypeEnum.PASSWORD, refreshToken);
if (StringUtils.isEmpty(accessToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token expired");
}
return ResponseEntity.ok(accessToken);
} catch (Exception e) {
log.error("刷新token异常:{}", e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token error");
}
}
}
3235

被折叠的 条评论
为什么被折叠?



