1通过@ConfigurationProperties 读取配置文件的配置,允许用户自定义规则
@ConfigurationProperties(prefix = "security.config")
@Data
public class SecurityConfigProperties {
/**
* token请求头名称
* */
private String tokenHeader;
/**
* token加解密使用的密钥
* */
private String tokenSecret;
/**
* token过期时间(秒)
* */
private Long tokenExpiration;
/**
* 访问路径白名单,无需登陆即可访问
* */
private List<String> ignoreUrls;
}
2 创建JWT工具类,用于生成和解析token
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
//存入token内的用户名的kye
private static final String CLAIM_KEY_USERNAME = "sub";
//存入token的创建时间
private static final String CLAIM_KEY_CREATED = "created";
@Autowired
private SecurityConfigProperties securityConfigProperties;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
//token加密密钥
String tokenSecret = securityConfigProperties.getTokenSecret();
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, tokenSecret )
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
//token加密密钥
String tokenSecret = securityConfigProperties.getTokenSecret();
try {
claims = Jwts.parser()
.setSigningKey(tokenSecret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
long expiration = securityConfigProperties.getTokenExpiration();
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是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getCla3 imsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
3 继承WebSecurityConfigurerAdapter类,配置security权限校验逻辑
@Configuration
@Slf4j
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@ConditionalOnBean(UserDetailsService.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 必须实现此接口,用于通过用户名获取当前登陆账号状态
* */
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 当访问接口没有权限时,如要自定义的返回结果,应实现 AccessDeniedHandler 接口
* */
@Autowired(required = false)
private AccessDeniedHandler accessDeniedHandler;
/**
* 当未登录或者token失效访问接口时,如要自定义的返回结果,应实现 AuthenticationEntryPoint 接口
* */
@Autowired(required = false)
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private SecurityConfigProperties securityConfigProperties;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//允许不用登陆即可访问的url
List<String> ignoreUrls = securityConfigProperties.getIgnoreUrls();
log.info("允许匿名访问的接口==>"+ignoreUrls);
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 允许对于网站静态资源的无授权访问
.antMatchers(HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
//将配置的路径设置为无权即可访问
.antMatchers(ignoreUrls.toArray(new String[0]))
.permitAll()
//跨域请求会先进行一次options请求
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest()
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
if(accessDeniedHandler!=null && authenticationEntryPoint !=null){
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
4 实现 userDetailService和
@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里为了易懂写死了,应该按需求查询数据库进行校验
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
MyUserDetails userDetails = new MyUserDetails();
if("张三".equals(username)){
userDetails = new MyUserDetails();
userDetails.setPassword(passwordEncoder.encode("zs"));
userDetails.setUsername("张三");
ArrayList<GrantedAuthority> arrayList = new ArrayList();
arrayList.add(new SimpleGrantedAuthority("zs"));
userDetails.setAuthorities(arrayList);
}else{
userDetails = new MyUserDetails();
userDetails.setPassword(passwordEncoder.encode("qt"));
userDetails.setUsername("其他");
ArrayList<GrantedAuthority> arrayList = new ArrayList();
arrayList.add(new SimpleGrantedAuthority("qt"));
userDetails.setAuthorities(arrayList);
}
return userDetails;
}
public String login(String username, String password) {
String token = null;
//密码需要客户端加密后传递
try {
UserDetails userDetails = loadUserByUsername(username);
if(!passwordEncoder.matches(password,userDetails.getPassword())){
throw new MyException(ResultStatus.ERROR);
}
if(!userDetails.isEnabled()){
throw new MyException(ResultStatus.ERROR);
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
} catch (AuthenticationException e) {
log.warn("登录异常:{}", e.getMessage());
}
return token;
}
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MyUserDetails implements UserDetails {
private List<GrantedAuthority> authorities;
private String username;
private String password;
@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;
}
}
5 创建过滤器获取请求的token后解析,如通过,将当前登陆信息存入SecurityContext
@ConditionalOnBean(UserDetailsService.class)
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
/**
* 必须实现此接口 用户登陆时通过用户名获取用户信息
* */
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private SecurityConfigProperties securityConfigProperties;
@Autowired
public PathMatcher pathMatcher;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String tokenName = securityConfigProperties.getTokenHeader();
//获取请求头
String authToken = request.getHeader(tokenName);
if (authToken != null) {
//获取当前登陆用户名称
String username = jwtTokenUtil.getUserNameFromToken(authToken);
//当前用户名不为空 并且是未登录状态
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//通过用户名查询用户
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
//验证Token
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
//创建凭证
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//放入凭证
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}