了解
开发
依赖配置
springboot使用最新版2.5.3
<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>
配置
常量配置
public interface Const {
//TOKEN
/**
* 令牌 header key
*/
String TOKEN_HEADER = "Authorization";
/**
* 令牌前缀
*/
String TOKEN_PREFIX = "Bearer ";
/**
* token 加密密钥
*/
String TOKEN_SECRET = "SpringSecurityAndJWT";
/**
* token 过期时间
*/
long TOKEN_EXPIRE_TIME = 7200;
}
用户对象
要使用 Spring Security 实现对用户的权限控制,
首先需要实现一个简单的 User 对象实现 UserDetails 接口,
UserDetails 接口负责提供核心用户的信息,
如果你只需要用户登陆的账号密码,不需要其它信息,如验证码等,那么你可以直接使用 Spring Security 默认提供的 User 类,而不需要自己实现
该用户对象是在登录成功后可以通过security的对象进行获取到的信息
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 7481749302887905270L;
private String username;
private String password;
private Boolean rememberMe;
private String captcha;
private String token;
private Long loginTime;
private Long expireTime;
private Set<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
这个就是我们要使用到的 User 对象,其中包含了【记住我,验证码】等登陆信息,
因为 Spring Security 整合 Jwt 本质上就是用自己自定义的登陆过滤器,去替换 Spring Security 原生的登陆过滤器,
这样的话,原生的记住我功能就会无法使用,所以我在 User 对象里添加了记住我的信息,用来自己实现这个功能
JWT 令牌认证工具
/**
* @author xupu
* @Description 处理Token认证过程中的验证和请求
* @Date 2021-10-14 17:16
*/
public class TokenAuthentication {
/**
* 设置登陆成功后令牌返回
*/
public static void addAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// 获取用户登陆角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 遍历用户角色
StringBuffer stringBuffer = new StringBuffer();
authorities.forEach(authority -> {
stringBuffer.append(authority.getAuthority()).append(",");
});
long expirationTime = Const.TOKEN_EXPIRE_TIME;
int cookExpirationTime = -1;
// 处理登陆附加信息
LoginUser loginDetails = (LoginUser) authentication.getDetails();
if(loginDetails.getRememberMe() != null && loginDetails.getRememberMe()) {
expirationTime = Const.REMEMBER_ME_EXPIRE_TIME * 1000;
cookExpirationTime = Const.REMEMBER_ME_EXPIRE_TIME;
}
//生成jwt
String jwt = Jwts.builder()
// Subject 设置用户名
.setSubject(authentication.getName())
// 设置用户权限
.claim("authorities", stringBuffer)
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
// 签名算法
.signWith(SignatureAlgorithm.HS512, Const.TOKEN_SECRET).compact();
Cookie cookie = new Cookie(Const.COOKIE_TOKEN, jwt);
cookie.setHttpOnly(true); //使用 HTTP Only 的 Cookie 可以有效防止 XSS 攻击
cookie.setPath("/");
cookie.setMaxAge(cookExpirationTime);
response.addCookie(cookie);
// 向前端写入数据
ServletUtils.renderString(response, JSON.toJSONString(R.ok(loginDetails)));
}
/**
* 对请求的验证
* 如果用户的 JWT 解析正确,则向 Spring Security 返回 usernamePasswordAuthenticationToken
* 用户名密码验证令牌,告诉 Spring Security 用户所拥有的权限,并放到当前的 Context中,然后执行过滤链使请求继续执行下去
*/
public static Authentication getAuthentication(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, Const.COOKIE_TOKEN);
String token = cookie != null ? cookie.getValue() : null;
if(token != null) {
Claims claims = Jwts.parser().setSigningKey(Const.TOKEN_SECRET).parseClaimsJws(token).getBody();
// 获取用户权限
Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
String userName = claims.getSubject();
if(userName != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, authorities);
usernamePasswordAuthenticationToken.setDetails(claims);
return usernamePasswordAuthenticationToken;
}
return null;
}
return null;
}
}
工具类方法
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
response.setStatus(200);
out.print(string);
} catch(IOException e) {
e.printStackTrace();
} finally {
assert out != null;
out.flush();
out.close();
}
}
Token过滤器
众所周知,Spring Security 是借助一系列的 Servlet Filter 来来实现提供各种安全功能的,
所以我们要使用 JWT 就需要自己实现两个和 JWT 有关的过滤器
- 一个是用户登录的过滤器:
在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个 token 返回给客户端,登录失败则给前端一个登录失败的提示。
- 第二个是其他请求过滤器:
是当其他请求发送来,校验 token 的过滤器,如果校验成功,就让请求继续执行。否则返回校验不通过的提示
security配置类
这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter
的部分 configure
配置,来实现用户自定义的部分。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
}
@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.POST, "/authentication/**").permitAll()
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
JWT工具类
主要是集成一些基础的功能,比如生成token,验证token,刷新token等功能
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据claims生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载claims
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if (StrUtil.isEmpty(oldToken)) {
return null;
}
String token = oldToken.substring(tokenHead.length());
if (StrUtil.isEmpty(token)) {
return null;
}
//token校验不通过
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
//如果token已经过期,不支持刷新
if (isTokenExpired(token)) {
return null;
}
//如果token在30分钟之内刚刷新过,返回原token
if (tokenRefreshJustBefore(token, 30 * 60)) {
return token;
} else {
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
*
* @param token 原token
* @param time 指定时间(秒)
*/
private boolean tokenRefreshJustBefore(String token, int time) {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
//刷新时间在创建时间的指定时间内
if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
return true;
}
return false;
}
}
请求过滤器
对前端发起的请求进行过滤,用于对token进行处理
/**
* JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
业务逻辑代码
登录
@Override
public String login( String username, String password ) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}
注册
@Override
public User register( User userToAdd ) {
final String username = userToAdd.getUsername();
if( userRepository.findByUsername(username)!=null ) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword( encoder.encode(rawPassword) );
return userRepository.save(userToAdd);
}