SpringSecurity构建基于JWT的登录认证实现
本文将介绍如何使用SpringSecurity框架,在基于JWT的前后端分离应用中,实现登录认证功能。
准备工作
在开始介绍实现方案之前,我们需要准备好以下工具和环境:
- JDK 8 及以上版本
- Maven 及其配置
- Spring Boot
- Spring Security
- JWT
Step 1: 创建项目
首先,我们需要创建一个Spring Boot项目,可以使用Spring Initializr来快速创建。
Step 2: 添加依赖
在pom.xml文件中,添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Step 3: 配置Spring Security
在Spring Boot项目中,Spring Security的默认配置并不适合前后端分离应用。因此,我们需要自定义Spring Security配置,修改默认配置。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationTokenFilter(jwtTokenUtil), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在上面的配置中,我们禁用了默认的CSRF保护,开启了自定义的认证过滤器JwtAuthenticationTokenFilter
,并使用了无状态的Session管理策略。
Step 4: 实现UserDetailsService
在Spring Security中,UserDetailsService
用于通过用户名从数据库中获取用户信息,包括密码和角色等信息。我们需要实现自己的UserDetailsService
接口,在其中查询数据库并返回UserDetails
对象。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在!");
}
return new UserDetail(user);
}
}
Step 5: 实现JWT
JWT是一种基于JSON的开放标准(RFC 7519),用于在网络上传递声明。JWT可以通过一些算法来实现签名,从而保证数据在传输过程中不被篡改。我们需要实现JWT工具类JwtTokenUtil
,来生成和验证JWT Token。
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_ROLES = "roles";
private String secret = "jwt_secret";
private Long expiration = 604800L;
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Date getCreatedDateFromToken(String token) {
Date created;
try {
Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
private Claims generateClaims(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_ROLES, roles);
return claims;
}
public String generateToken(String username, List<String> roles) {
Claims claims = generateClaims(username, roles);
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean canTokenBeRefreshed(String token) {
return (!isTokenExpired(token));
}
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
public Boolean validateToken(String token, UserDetails userDetails) {
UserDetail userDetail = (UserDetail) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(userDetail.getUsername()) && !isTokenExpired(token));
}
}
Step 6: 自定义认证过滤器
在Spring Security中,认证过滤器UsernamePasswordAuthenticationFilter
通常在处理表单登录和Basic认证时使用。在前后端分离应用程序中,我们需要自定义认证过滤器来处理JWT认证。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final String TOKEN_HEADER = "Authorization";
private JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String tokenHeader = request.getHeader(TOKEN_HEADER);
if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
String authToken = tokenHeader.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
Step 7: 添加登录接口
接下来,我们需要在后台代码中添加登录功能。用户在前端输入账号和密码,后端验证账号密码是否正确。如果认证成功,则返回JWT Token给前端。
@RestController
public class JwtAuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping(value = "/auth/login")
public ResponseResult<LoginSuccessResponse> login(@RequestBody AuthRequest request) {
// 使用Spring Security进行用户名和密码验证
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());
Authentication authentication = authenticationManager.authenticate(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成JWT Token
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtTokenUtil.generateToken(userDetails.getUsername(), userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
LoginSuccessResponse successResponse = new LoginSuccessResponse();
successResponse.setToken(token);
successResponse.setUsername(userDetails.getUsername());
successResponse.setRoles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return ResponseResult.success(successResponse);
}
static class AuthRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
static class LoginSuccessResponse {
private String username;
private String token;
private List<String> roles;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
}
}
Step 8: 测试
我们可以使用PostMan来测试登录API是否正常工作。将用户名和密码填入请求体中,发送HTTP POST请求到/auth/login,如果认证成功,则会返回JWT Token。
POST /auth/login HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"username": "admin",
"password": "123456"
}
示例
示例一:GitHub项目
我编写了一个GitHub项目,其中包括了本文中所介绍的完整代码,可以在此处查看:https://github.com/samniya/spring-security-jwt-demo
示例二:CSDN博客
我在CSDN中发表了一篇博客文章,介绍了如何使用Spring Security和JWT实现基于Token的RESTful API认证。可以在此处查看:https://blog.csdn.net/wangsongtao22/article/details/104442766
总结
在本文中,我们介绍了如何使用Spring Security框架和JWT,实现基于Token的RESTful API认证。通过使用自定义的认证过滤器和JWT工具类,我们使得Spring Security与前端SPA的结合更加完美,实现了无状态的认证和授权。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringSecurity构建基于JWT的登录认证实现 - Python技术站