问题 :用security+jwt 方案进行登录认证。那么登录成功后,生成的token如何防止被窃取
首次登录,还是以传统的表单登录模式,所以我们可以直接使用UsernamePasswordAuthenticationFilter来拦截/login进行认证处理,不过我这里自定义了一个JwtAuthenticationFilter,其功能跟UsernamePasswordAuthenticationFilter差不多
JwtAuthenticationFilter 继承 UsernamePasswordAuthenticationFilter 并重写 attemptAuthentication
package com.yzm.security08.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JwtAuthenticationFilter() {
super();
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new AuthenticationServiceException("用户名密码错误");
}
// 构造未鉴权的JwtAuthenticationToken 对象
JwtAuthenticationToken authToken = new JwtAuthenticationToken(username, password);
this.setDetails(request, authToken);
return this.getAuthenticationManager().authenticate(authToken);
}
}
JwtAuthenticationToken 继承 UsernamePasswordAuthenticationToken
package com.yzm.security08.config;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
public JwtAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
身份认证 Provider,获取UserDetails对象,校验密码,构造鉴权的Authentication对象
package com.yzm.security08.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
@Slf4j
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public JwtAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
String username = (String) authenticationToken.getPrincipal();
String password = (String) authenticationToken.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails == null) throw new UsernameNotFoundException("账号异常");
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("凭证异常");
}
// 构造鉴权的JwtAuthenticationToken对象
JwtAuthenticationToken resultToken = new JwtAuthenticationToken(username, password,
userDetails.getAuthorities());
resultToken.setDetails(authentication.getDetails());
return resultToken;
}
}
登录认证成功之后,返回token
package com.yzm.security08.config;
import com.yzm.common.utils.HttpUtils;
import com.yzm.security08.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (authentication instanceof JwtAuthenticationToken) {
log.info("登录认证");
JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
String username = (String) authenticationToken.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put(JwtUtils.USERNAME, username);
String token = JwtUtils.generateToken(map);
HttpUtils.successWrite(response, token);
}
}
}
为了防止他人截获或盗用登录令牌(token),可以采取以下措施:
使用https协议传输数据,以确保通信过程中的加密安全性。
设置token过期时间,限制token的有效时间。
采用密钥对token进行签名,防止他人伪造。密钥只在服务器端存储,不要泄漏。
不在代码中明文显示密钥,可以采用将密钥写在配置文件中,然后读取配置文件获取密钥。
不能将token存储在客户端的cookie和localStorage中,避免被恶意攻击者获取。
需要对请求头进行验证,只有携带正确的token且未过期才能进行相关操作。
可以参考下面的代码:
// 配置JWT生成工具类
public class JwtTokenUtils {
// 签名密钥,应与服务器端的密钥相同,保持安全
private static final String secret = "yourSecretKey";
// 过期时间,这里设置为2小时
private static final long expireTime = 2 * 60 * 60 * 1000;
/**
* 生成token
*/
public static String generateToken(String username, List<String> roles) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + expireTime);
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
claims.put("roles", roles);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
/**
* 验证token是否有效
*/
public static boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception ex) {
return false;
}
}
/**
* 从token中获取用户名
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.get("username", String.class);
}
/**
* 从token中获取用户角色
*/
public static List<String> getRoles(String token) {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
List<String> roles = (List<String>) claims.get("roles", List.class);
return roles;
}
}
// 配置JWT的过滤器
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
if (token != null && JwtTokenUtils.validateToken(token)) {
String username = JwtTokenUtils.getUsername(token);
List<String> roles = JwtTokenUtils.getRoles(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
// 配置权限管理器
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/api/authenticate").permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginUserDetailsService);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}