一.简单案例 原始页面 对controller起保护作用
原始页面 使用默认用户名和密码
(1)引入jar包
<!-- 引入spring-security 的starter依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
(2)登录用户名:user 控制台中有密码
SecurityConfig总配置
package com.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.entity.SysMenu;
import com.mapper.SysMenuMapper;
import com.service.DynamicSecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//使用注解对权限进行控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/*//redis
@Autowired
private RedisSecurityContextRepository redisSecurityContextRepository;*/
//jwt
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private SysMenuMapper sysMenuMapper;
//创建 DynamicSecurityService 实现
@Bean
public DynamicSecurityService securityService(){
return new DynamicSecurityService(){
@Override
public Map<String, ConfigAttribute> loadDataSource() {
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.isNotNull("perms")
.gt("length(perms)","0")
.select("url","perms");
List<SysMenu> sysMenus = sysMenuMapper.selectList(wrapper);
Map<String,ConfigAttribute> map = new HashMap<>();
for (SysMenu sysMenu : sysMenus) {
map.put(sysMenu.getUrl(),new org.springframework.security.access.SecurityConfig(sysMenu.getPerms()));
}
return map;
}
};
}
// 创建自定义读取数据库配置
@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(){
return new DynamicSecurityMetadataSource();
}
// 创建选举器对象
@Bean
public AccessDecisionVoter dynamicVoter(){
return new DynamicSecurityVoter();
}
//AccessDecisionManager 使用自定义的选举器
@Bean
public AccessDecisionManager dynamicSecurityAccessDecisionManager(){
return new AffirmativeBased(Arrays.asList(dynamicVoter()));
}
//创建自定义过滤器
@Bean
public DynamicSecurityFilter dynamicSecurityFilter(){
return new DynamicSecurityFilter();
}
//对白名单路径放行
@Bean
public IgnoreUrlsProperties ignoreUrlsProperties() {
return new IgnoreUrlsProperties();
}
//在controller中模拟Filter 使用AuthenticationManager完成认证
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//替换默认的密码格式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//将自定义的过滤器添加到过滤链中
http.addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加过滤器到指定过滤器之后
//http.addFilterBefore(new MyFilter(), WebAsyncManagerIntegrationFilter.class);
// 添加过滤器到指定过滤器之前
//http.addFilterAfter(new MyFilter(), UsernamePasswordAuthenticationFilter.class);
//super.configure(http); 自己设置密码时,要注销
//对UsernamePasswordAuthenticationFilter 进行定制 处理登录认证
/*http.formLogin()
.loginPage("/login.html") //指定登录页
.loginProcessingUrl("/login") //只要指定登录页 就必须提供路径 并且和表单一致
.usernameParameter("username") //指定表单中提供的用户名 key
.passwordParameter("password") //指定表单中提供的密码 key
.defaultSuccessUrl("/main.html")//认证成功:重定向到主页面
.failureUrl("/register.html")//认证成功:重定向到注册页面
//...
;*/
//对LogoutFilter 进行定制 处理登出操作
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//前后端分离
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().println("登出成功");
}
})
//...
;
//对ExceptionTranslationFilter 进行定制 处理认证和鉴权失败
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//前后端未分离
//httpServletResponse.sendRedirect("/login.html");//认证失败后返回登录页面
//前后端分离
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().println("你还没有登录");
}
})
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().println("你没有权限访问");
}
})
//...
;
//对FilterSecurityInterceptor 进行定制 认证和鉴权
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry urlRegistry = http.authorizeRequests();
//对白名单进行遍历
for (String url :ignoreUrlsProperties().getUrls()){
urlRegistry.mvcMatchers(url).permitAll();
}
//设置权限
//urlRegistry.mvcMatchers("/hello").hasAnyAuthority("aa");
urlRegistry.anyRequest().authenticated() //所有请求都拦截 必须认证
//...
;
//对CsrfFilter 进行定制 跨域请求伪造攻击的防护
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);; //关闭
/*//设置自定义的SecurityContext持久化机制
http.securityContext()
.securityContextRepository(redisSecurityContextRepository);*/
}
}
连接数据库
Service实现UserDetailsService接口
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysMenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", s);
SysUser sysUser = userMapper.selectOne(queryWrapper);
if (sysUser==null){
throw new UsernameNotFoundException("用户名或密码错误");
}
//设置权限操作
List<String> perms = menuMapper.selectPermsByUserId(sysUser.getId());
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(perms.toArray(new String[]{}));
return new User(sysUser.getUsername(), sysUser.getPassword(),authorityList);
}
}
对密码进行加密设置
//替换默认的密码格式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
二.Spring Security的认证流程
三.自定义登录接口
controller中的编写
//认证前进行封装 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
//使用 AuthenticationManager 与数据库进行对比
Authentication authenticate = authenticationManager.authenticate(authentication);
//保存认证后的用户信息
SecurityContextHolder.getContext().setAuthentication(authenticate);
前端分离和未分离
未分离:接口返回为页面
分离:接口返回为json数据
四.白名单的处理
application中的配置
#配置白名单 放行路径
secure.ignored.urls=/放行路径
自定义Properties类读取配置
@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsProperties {
private List<String> urls = new ArrayList<>();
}
SecurityConfig对白名单路径放行
@Bean
public IgnoreUrlsProperties ignoreUrlsProperties() {
return new IgnoreUrlsProperties();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置白名单
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsProperties().getUrls()) {
registry.antMatchers(url).permitAll();
}
...
}
五.Spring Security的授权
基于路径配置的权限控制
//对FilterSecurityInterceptor 进行定制
http.authorizeRequests()
//允许匿名访问登录接口 ant表达式 ?匹配一个字符 *匹配任意个字符 **匹配任意个目录
.antMatchers("/user/form/login").permitAll()
.anyRequest().authenticated();所有请求都拦截 必须认证
基于方法注解的权限控制
1.@EnableGlobalMethodSecurity(prePostEnabled = true)//使用注解对权限进行控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
2.在controller中使用注解
@PreAuthorize("hasRole('admin')")
public String hello(){
return "";
}
六.Spring Security动态权限控制
自定义过滤器拦截
package com.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
public class DynamicSecurityFilter extends FilterSecurityInterceptor {
@Autowired
private IgnoreUrlsProperties ignoreUrlsProperties;
//重写父类set方法注入属性
@Override
@Autowired
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Autowired
public void setSecurityMetadataSource(DynamicSecurityMetadataSource newSource) {
super.setSecurityMetadataSource(newSource);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//对白名单放行
List<String> urls = ignoreUrlsProperties.getUrls();
PathMatcher pathMatcher=new AntPathMatcher();//路径匹配器
for (String url : urls) {
if (pathMatcher.match(url,((HttpServletRequest)request).getRequestURI())) {
chain.doFilter(request,response);
return;
}
}
// 针对跨域时 OPTIONS请求直接放行
if (((HttpServletRequest) request).getMethod().equals(HttpMethod.OPTIONS)) {
chain.doFilter(request,response);
return;
}
super.doFilter(request, response, chain);
}
}
读取数据库中的设置
package com.config;
import com.service.DynamicSecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.annotation.PostConstruct;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private DynamicSecurityService service;
private Map<String, ConfigAttribute> configAttributeMap;
@PostConstruct//在初始化后执行1次,从数据库中加载权限配置信息
public void loadDataSource() {
configAttributeMap = this.service.loadDataSource();
//遍历数据
/*configAttributeMap.forEach((k, v) -> {
System.out.println(k + "-->" + v);
});*/
}
//获取本次需要的权限
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取本次请求的uri路径
String url = ((FilterInvocation) o).getRequestUrl();
String path = null;
try {
path = new URI(url).getPath();
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalArgumentException(e);
}
//遍历configAttributeMap,获取所需要的权限
PathMatcher pathMatcher = new AntPathMatcher();
List<ConfigAttribute> configAttributeList = new ArrayList<>();
for (Map.Entry<String, ConfigAttribute> entry : configAttributeMap.entrySet()) {
String key = entry.getKey();
if (pathMatcher.match(key, path)) {
configAttributeList.add(entry.getValue());
}
}
return configAttributeList;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
3.自定义DynamicSecurityVoter
package com.config;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
public class DynamicSecurityVoter implements AccessDecisionVoter<Object> {
@Override
//判断ConfigAttribute是否可以使用当前Voter进行投票判断
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null; //需要权限 true
}
@Override
//判断Filter中构建的安全对象是否为当前Voter支持
public boolean supports(Class<?> clazz) {
return true;
}
//authentication 用户登录后的认证对象 包含 用户名 密码 权限
//Collection 代表当前路径下的权限列表
//返回值 代表是否 通过 1 通过 -1 不通过 0 弃权
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
//如果本次访问没有权限要求,则弃权
if (CollectionUtils.isEmpty(attributes)) {
return AccessDecisionVoter.ACCESS_ABSTAIN;
}
//如果用户没有任何权限,则拒绝
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (CollectionUtils.isEmpty(authorities)) {
return AccessDecisionVoter.ACCESS_DENIED;
}
//遍历所需权限,判断当前用户是否拥有需要的权限
for (ConfigAttribute needAttribute : attributes) {
//遍历用户持有的权限
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().trim().equalsIgnoreCase(needAttribute.getAttribute().trim())) {
return AccessDecisionVoter.ACCESS_GRANTED;
}
}
}
//没有匹配的权限,则拒绝
return AccessDecisionVoter.ACCESS_DENIED;
}
}
4.通过SecurityConfig将三者联系起来
//创建 DynamicSecurityService 实现
@Bean
public DynamicSecurityService securityService(){
return new DynamicSecurityService(){
@Override
public Map<String, ConfigAttribute> loadDataSource() {
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.isNotNull("perms")
.gt("length(perms)","0")
.select("url","perms");
List<SysMenu> sysMenus = sysMenuMapper.selectList(wrapper);
Map<String,ConfigAttribute> map = new HashMap<>();
for (SysMenu sysMenu : sysMenus) {
map.put(sysMenu.getUrl(),new org.springframework.security.access.SecurityConfig(sysMenu.getPerms()));
}
return map;
}
};
}
// 创建自定义读取数据库配置
@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(){
return new DynamicSecurityMetadataSource();
}
// 创建选举器对象
@Bean
public AccessDecisionVoter dynamicVoter(){
return new DynamicSecurityVoter();
}
//AccessDecisionManager 使用自定义的选举器
@Bean
public AccessDecisionManager dynamicSecurityAccessDecisionManager(){
return new AffirmativeBased(Arrays.asList(dynamicVoter()));
}
//创建自定义过滤器
@Bean
public DynamicSecurityFilter dynamicSecurityFilter(){
return new DynamicSecurityFilter();
}
//将过滤器添加到该方法中
protected void configure(HttpSecurity http) throws Exception {
//将自定义的过滤器添加到过滤链中
http.addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}
七.认证信息的处理 通过session和cookie保存
原因:在分布式情况下 无法直接使用这种方式保存---相当于在不同客户端需要分别进行登录
1.使用redis解决
添加redis的starter依赖
<!-- 引入redis的starter依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
application添加redis连接配置
#配置redis
spring.redis.host=192.168.136.128
spring.redis.port=6379
配置RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key 序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value 序列化
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
// 注入连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置事务支持
redisTemplate.setEnableTransactionSupport(true);
// 让设置生效
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
自定义SecurityContextRepository
package com.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Arrays;
@Component
public class RedisSecurityContextRepository implements SecurityContextRepository {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
private String findAuthenticatedUuid(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length == 0){
return null;
}
return Arrays.stream(cookies).filter(c -> SPRING_SECURITY_CONTEXT_KEY.equalsIgnoreCase(c.getName())).findFirst().map(Cookie::getValue).orElse(null);
}
@Override
//获取redis中的认证信息
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
String uuid = findAuthenticatedUuid(request);
if(uuid == null){
return SecurityContextHolder.createEmptyContext();
}
//uuid有值
Object context = redisTemplate.opsForValue().get(uuid);
//判断是否到期 到期了为null
if(context == null){
return SecurityContextHolder.createEmptyContext();
}
response.setHeader("uuid",uuid);
return (SecurityContext) context;
}
//存储认证信息 redis
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
final Authentication authentication = context.getAuthentication();
//判断认证信息是否为空 或者是匿名用户
if (authentication == null || trustResolver.isAnonymous(authentication)) {
//SecurityContext is empty or contents are anonymous - context will not be stored in Redis
return;
}
//获取响应头的uuid
String uuid = response.getHeader("uuid");
if(uuid == null || uuid.isEmpty()){
return;
}
//存储到redis中
redisTemplate.opsForValue().set(uuid,context, Duration.ofMinutes(30));
}
@Override
//判断本次请求能否获取登录信息
public boolean containsContext(HttpServletRequest request) {
//请求中有uuid 通过这个uuid从redis中得到认证信息
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length==0){
return false;
}
String uuid = null;
for (Cookie cookie : cookies) {
if( cookie.getName().equals(SPRING_SECURITY_CONTEXT_KEY) ){
uuid = cookie.getValue();
}
}
if (uuid==null){
return false;
}
//说明client传递了uuid uuid有值
Object context = redisTemplate.opsForValue() .get(uuid);
if(context == null){
return false;
}
return true;
}
}
修改UserController
//认证前进行封装 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
//使用 AuthenticationManager 与数据库进行对比
Authentication authenticate = authenticationManager.authenticate(authentication);
//保存认证后的用户信息
SecurityContextHolder.getContext().setAuthentication(authenticate);
//生成用户登录的uuid,将其通过cookie响应回client
String uuid = UUID.randomUUID().toString();
response.setHeader("uuid",uuid);
Cookie cookie = new Cookie(SPRING_SECURITY_CONTEXT_KEY,uuid);
cookie.setPath("/");
SecurityConfig配置
@Autowired
private RedisSecurityContextRepository redisSecurityContextRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用csrf和session
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//设置自定义的SecurityContext持久化机制
http.securityContext()
.securityContextRepository(redisSecurityContextRepository)
}
2.使用jwt解决
原因:redis代替session存储信息 本质存储在后端 每次都要进行访问
jwt不是存储后端而是client jwt本质为一个加密的字符串
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
application配置密钥、过期时间以及使用的tokenHead
#配置JWT
#JWT存储的请求头
jwt.tokenHeader=Authorization
#JWT加解密使用的密钥
jwt.secret=bz-mall-admin-secret
#JWT的超期限时间(60*60*24*7)
jwt.expiration=604800
#JWT的标识前缀
jwt.tokenHead='Bearer '
引入工具类
package com.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
* Created by xushy on 2022年12月28日.
*/
@Component
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";
private static final String CLAIM_KEY_AUTHORITIES = "authorities";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 根据负责生成JWT的token
*/
public String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
public 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
*/
public boolean validateToken(String token) {
return getClaimsFromToken(token) != null && !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(Authentication authentication) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, authentication.getName());
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_AUTHORITIES,authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if(StringUtils.isEmpty(oldToken)){
return null;
}
String token = oldToken.substring(tokenHead.length());
if(StringUtils.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(new Date(created.getTime() + time*1000))){
return true;
}
return false;
}
public Authentication getAuthentication(String authToken) {
Claims claims = getClaimsFromToken(authToken);
String username = claims.getSubject();
List<String> authorities = claims.get(CLAIM_KEY_AUTHORITIES, List.class);
return new UsernamePasswordAuthenticationToken(username,"N/A",authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
}
}
自定义JwtAuthenticationTokenFilter
package com.config;
import com.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
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;
/**
* JWT登录授权过滤器
* Created by xushy on 2022年12月28日
*/
@Component
//一次请求只会执行该类一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@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);
//判断 authHeader 是否为空 并且格式是否正确
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer " 去除前缀
//String username = jwtTokenUtil.getUserNameFromToken(authToken);
//判断是不是程序颁发的
if (jwtTokenUtil.validateToken(authToken)) {
//从token中解析回认证信息,用于后续的授权管理
Authentication authentication = jwtTokenUtil.getAuthentication(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
//放行请求
chain.doFilter(request, response);
}
}
SecurityConfig配置
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
protected void configure(HttpSecurity http) throws Exception{
...
http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
...
}
修改UserController
//认证前进行封装 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
//使用 AuthenticationManager 与数据库进行对比
Authentication authenticate = authenticationManager.authenticate(authentication);
//保存认证后的用户信息
SecurityContextHolder.getContext().setAuthentication(authenticate);
//将用户认证信息转换为jwt,通过响应头传递给客户端
String token = jwtTokenUtil.generateToken(authenticate);
resp.setHeader(tokenHeader,tokenHead+token);