springboot使用JWT实现会话认证
最近在做一个项目的收尾,安全检测小组提出了一些需求:
在通信双方建立连接之前,应用系统应利用密码技术进行会话初始化验证,并对通信过程中的敏感信息字段进行加密。
本项目是springboot项目,主要功能是一个管理后台,为了解决这个需求,第一思路就是用户登陆之后给用户客户端一个码,用户客户端需要调用接口的时候带上这个码就可以进行访问了。
这样做是可以的,但是遇到的问题是这个接口都写完了,如果一个一个改,是很麻烦的,我上网查了一下,发现springboot的项目可以引入JWT去解决,下面详细地讲一下如何通过JWT去实现安全小组提出的需求。
打开pom文件,先把依赖加进去,工欲善其事必先利其器。
<!--加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
这其实和以前登陆的思路是一样的,前端输入账号和密码,后端进行匹配验证。那JWT是怎么做的呢,
1.以前是有一套用户的数据,现在也是要的,编写一个类重点是实现UserDetailsService接口。为什么要实现UserDetailsService接口,因为实现了UserDetailsService接口才可以更加方便的实现JWT后面的操作,减少很多自己原本需要做的东西
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import java.util.ArrayList; @Component public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //假数据
//具体的业务逻辑--例如数据库中获取账户信息详细信息等等 return new User( "root" , "admin" , new ArrayList<>()); } }
这个类实现了UserDetailsService接口,主要是用于入参 s(用户名),然后返回用户的详细信息,主要是new User(用户名, 密码, new ArrayList<>())。
ok到下一步,下一步就是后端匹配完用户准确之后,需要返回一个token回去,以后前端就拿着这个token问你讨数据。那这个token就靠下面这代码完成
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; //用于生成和解析jwt @Component public class JwtUtil { private String SECRET_KEY = "secret"; // 在生产环境中,使用强加密的密钥 public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String generateToken(String username) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, username); } private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Boolean validateToken(String token, String username) { final String extractedUsername = extractUsername(token); return (extractedUsername.equals(username) && !isTokenExpired(token)); } }
这个代码可以直接复制,我也是复制chatgpt的,有两个地方要留意哈,第一个是private String SECRET_KEY = "secret";这个是密钥,加密用,按照真实情况修改,但是这个应该是要写再配置文件里面,当然配置文件也是需要加密的,这个我有时间再写一下,在开发中也是比较常用的。另外是这个setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时有效期,设置token的有效期啊,就是多久需要重新登陆,我个认为有效期可以有效的解决重放攻击,但是这个也会在之后写一下具体的方案,估计是会专门单开一个安全的章节去写,系统的写一下。
ok那现在是匹配用户的类写了,生成token的类也写了,剩下的就是校验前端发送过来的token了
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private MyUserDetailsService userDetailsService; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
这个类主要是用于校验前端发送过来的token
ok那现在基本是齐活了,生成token的类,检验token的类,还有用户信息的类,现在需要把他们组装起来。
port org.springframework.beans.factory.annotation.Autowired; 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.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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter; @Override
//装配用户信息类 protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
@Bean
//配置跨域类,security会自动识别CorsFilter的类进行使用,如果存在多个则会优先使用corsFilter名称的类
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); // 允许发送Cookie信息
config.addAllowedOriginPattern("*"); // 允许所有来源的请求
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有HTTP方法
source.registerCorsConfiguration("/**", config); // 对所有路径生效
return new CorsFilter(source);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors().and() // 启用CORS配置
.authorizeRequests().antMatchers("/authenticate").permitAll() // 允许未经验证的访问,这个是登陆的接口,用户通过这个接口获取JWT
.anyRequest().authenticated() // 其他请求需要验证
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用无状态会话
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器
}
}
ok到现在基本完成了,再写一个登陆的类就完成了。
@RestController
@CrossOrigin
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private MyUserDetailsService userDetailsService;
@PostMapping("/authenticate")
@CrossOrigin
public Result createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
Result result = new Result();
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (AuthenticationException e) {
result.setData(MapUtils.map3("loginResult", false, "code", null));
return result;
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails.getUsername());
result.setData(MapUtils.map3("loginResult", true, "code", jwt));
return result;
}
}