我的相关文章:
springboot security 配置:juejin.cn/post/732527…
自定义 SecurityContext 存储策略:juejin.cn/post/732933…
主要依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--jedis作为连接池-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
安全配置
/**
* Spring Security 自动配置类,主要用于相关组件的配置
*
* @author LGC
*/
@RequiredArgsConstructor
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityConfiguration {
private final TokenService tokenService;
/**
* 未认证(未登录)自定义处理器 Bean
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPointImpl();
}
/**
* 无权限访问自定义处理器 Bean
*/
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandlerImpl();
}
/**
* 注册认证管理器
*/
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 密码加密器 bean
*
* @return 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注册 Token 过滤器
*
* @param tokenService
* @return
*/
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter(TokenService tokenService) {
return new TokenAuthenticationFilter(tokenService);
}
@Bean(value = "ss")
public SecurityService securityService() {
return new SecurityService();
}
/**
* 更改SecurityContextHolder的SecurityContext存储策略
* <p>
* MethodInvokingBean将自己注册为Bean,他的子类MethodInvokingFactoryBean也将注册为Bean的实例,
* 但是实际通过getBean调用的时候会将MethodInvokingFactoryBean.getObject作为结果返回给调用的对象
*
* @return
*/
@Bean
public MethodInvokingBean methodInvokingBean() {
MethodInvokingBean methodInvokingBean = new MethodInvokingBean();
methodInvokingBean.setTargetClass(SecurityContextHolder.class);
methodInvokingBean.setTargetMethod("setStrategyName");
methodInvokingBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
return methodInvokingBean;
}
}
/**
* @author LGC
*/
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfigurationAdapter {
private final SecurityProperties securityProperties;
/**
* token认证过滤器 Bean
*/
private final TokenAuthenticationFilter authenticationTokenFilter;
/**
* 未认证(未登录)自定义处理器
*/
private final AuthenticationEntryPoint authenticationEntryPoint;
/**
* 权限不够处理器 Bean
*/
private final AccessDeniedHandler accessDeniedHandler;
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 一般前后端分离基于token,都会配置开启跨域,CSRF禁用,session无状态
.cors()// 开启跨域
.and()
.csrf().disable()// CSRF禁用,CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果token 和服务端的 token 匹配成功,则正常访问
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 基于token机制,配置Session无状态
.and()
.headers().frameOptions().disable()//禁用X-Frame-Options
.and()
// 自定义的 Spring Security 处理器
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)// 配置自定义未认证(未登录)处理器,一般都会配置上并由前端控制跳转到登录页,未配置则自动跳转到默认登陆页
.accessDeniedHandler(accessDeniedHandler)// 配置无权限访问自定义处理器,未配置则自动跳转到自己创建403页面,如未创建自己403页面则直接报错
.and()
// 配置请求地址的权限
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()// 静态资源,可匿名访问
.antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()// 所有用户可访问
.antMatchers("/api/user/login").permitAll()// 所有用户可访问
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
;
return httpSecurity.build();
}
}
/**
* 安全配置配置类
*
* @author LGC
*/
@Data
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
private List<String> permitAllUrls = Collections.emptyList();
}
自定义处理器
/**
* 未认证(未登录)自定义处理器
*
* @author LGC
*/
@Slf4j
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
String content = "未认证(未登录)自定义处理";
ServletUtil.write(response, content, "application/json;charset=UTF-8");
}
}
/**
* 无权限访问自定义处理器
*
* @author LGC
*/
@Slf4j
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException, ServletException {
String content = "无权限访问自定义处理";
ServletUtil.write(response, content, "application/json;charset=UTF-8");
}
}
自定义token拦截器
/**
* Token 过滤器,验证 token 的有效性
*
* @author LGC
*/
@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenService tokenService;
@Override
@SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 获取登录用户信息
LoginUser loginUser = tokenService.getLoginUserFromRequest(request);
if (loginUser != null) {
// 验证令牌有效期,相差不足 20 分钟,自动刷新缓存
tokenService.verifyToken(loginUser);
// 创建Authentication并设置到上下文
setLoginUser(loginUser, request);
}
// 继续执行下一个过滤器
chain.doFilter(request, response);
}
/**
* 创建Authentication并设置到上下文
*
* @param loginUser 登录用户信息
* @param request 请求
*/
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
// 创建Authentication并设置到上下文
Authentication authentication = buildAuthentication(loginUser, request);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 构建认证后用户信息
*
* @param loginUser 登录用户信息
* @param request 请求
* @return Authentication
*/
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
loginUser, null, loginUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return authentication;
}
}
全局异常处理
/**
* 全家异常处理
*
* @author LGC
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 使用 @PreAuthorize 校验权限不通过时,没有访问权限,就会抛出 AccessDeniedException 异常
*
* @param e 访问拒绝异常
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<?> handleAuthorizationException(AccessDeniedException e) {
log.error(e.getMessage());
return Result.error("没有权限,请联系管理员授权");
}
/**
* 基础异常 BaseException
*/
@ExceptionHandler(value = BaseException.class)
public Result<?> serviceExceptionHandler(BaseException ex) {
return Result.error(ex.getMessage());
}
// ... 省略其它异常类的处理的方法
}
用户服务
/**
* 用户服务
* 模拟从DB获取用户
*
* @author LGC
*/
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
// 模拟数据库
private final Map<String, LoginUser> userMap = new HashMap<>();
/**
* 模拟数据库初始化两个用户
* 1.管理用户,管理员角色
* 2.普通用户,普通角色,具有更新、新增用户权限
*/
@PostConstruct
public void init() {
LoginUser admin = LoginUser.builder()
.username("admin")
.password(passwordEncoder.encode("123456"))
.roles(Sets.set("admin"))
.build();
LoginUser normal = LoginUser.builder()
.username("normal")
.password(passwordEncoder.encode("123456"))
.roles(Sets.set("normal"))
.permissions(Sets.set("system:user:update", "system:user:add")).build();
userMap.put(admin.getUsername(), admin);
userMap.put(normal.getUsername(), normal);
}
@Override
public LoginUser loadUserByUsername(String username) throws UsernameNotFoundException {
return userMap.get(username);
}
}
Token 服务
/**
* token服务
*
* @author LGC
*/
@Service
public class TokenService {
public static final String LOGIN_USER_KEY = "u:login:";
// token 前缀
public static final String TOKEN_PREFIX = "Bearer ";
// 20分钟时间
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private long expireTime;
// 令牌自定义标识
@Value("${token.header}")
private String header;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(LOGIN_USER_KEY, loginUser.getUsername());
return createToken(claims);
}
/**
* 刷新令牌过期时间
*
* @param loginUser
*/
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * 1000);
// 用户名作为key
String userKey = getTokenKey(loginUser.getUsername());
redisTemplate.opsForValue().set(userKey, loginUser);
redisTemplate.expire(userKey, expireTime, TimeUnit.MINUTES);
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUserFromRequest(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getRequestToken(request);
if (StrUtil.isNotEmpty(token)) {
// 解析 JWT 的 Token,获取username
String username = getUserNameFromToken(token);
String userKey = getTokenKey(username);
return (LoginUser) redisTemplate.opsForValue().get(userKey);
}
return null;
}
/**
* 验证令牌有效期,相差不足 20 分钟,自动刷新缓存
*
* @param loginUser 用户
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
// 相差不足 20 分钟,自动刷新缓存
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
/**
* 获取请求携带的令牌
*
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
token = token.replace(TOKEN_PREFIX, "");
}
return token;
}
/**
* 获取token redis key
*
* @param username
* @return
*/
private String getTokenKey(String username) {
return LOGIN_USER_KEY + username;
}
/**
* jwt生成token
*
* @param claims
* @return
*/
private String createToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
// 过期时间,使用redis控制,如果jwt设置过期时间,每次生成的token都不一样
// .setExpiration(new Date(System.currentTimeMillis() + expireTime * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* jwt解析token
*
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* jwt解析token 用户名
*
* @param token
* @return
*/
private String getUserNameFromToken(String token) {
return (String) getClaimsFromToken(token).get(LOGIN_USER_KEY);
}
}
用户认证服务
/**
* 用户认证服务
*
* @author LGC
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;
private final UserService userService;
private final TokenService tokenService;
public String login(String username, String password) {
/**
* 方式1 认证用户,并保存到上下文
*/
// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// Authentication authenticate = null;
// try {
// authenticate = authenticationManager.authenticate(authenticationToken);
// } catch (Exception e) {
// if (e instanceof BadCredentialsException) {
// throw new BaseException("用户名或者密码输入错误,登录失败");
// } else if (e instanceof DisabledException) {
// throw new BaseException("账户被禁用,登录失败");
// } else if (e instanceof CredentialsExpiredException) {
// throw new BaseException("密码过期,登录失败");
// } else if (e instanceof AccountExpiredException) {
// throw new BaseException("账户过期,登录失败");
// } else if (e instanceof LockedException) {
// throw new BaseException("账户被锁定,登录失败");
// }
// }
// SecurityContextHolder.getContext().setAuthentication(authenticate);
/**
* 方式2 手动认证用户,并保存到上下文(这个我们项目用的比较多)
*/
LoginUser loginUser = userService.loadUserByUsername(username);
if (loginUser == null) {
throw new BaseException("账户不存在");
}
if (!passwordEncoder.matches(password, loginUser.getPassword())) {
throw new BaseException("密码输入错误,登录失败");
}
if (!loginUser.isEnabled()) {
throw new BaseException("账户被锁定,登录失败");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 创建token
return tokenService.createToken(loginUser);
}
}
自定义授权校验服务
/**
* 自定义授权校验Bean 命名为ss
* 判断是否有对应权限,可自行实现
*
* @author LGC
*/
public class SecurityService {
public boolean hasPermission(String permission) {
return hasAnyPermissions(permission);
}
public boolean hasAnyPermissions(String... permissions) {
return false;
}
public boolean hasRole(String role) {
return hasAnyRoles(role);
}
public boolean hasAnyRoles(String... roles) {
return false;
}
public boolean hasScope(String scope) {
return hasAnyScopes(scope);
}
public boolean hasAnyScopes(String... scope) {
return false;
}
}
用户控制层
/**
* 用户控制器
* 调用登录接口获取token http://192.168.88.54:9023/api/user/login?username=admin&password=123456
* 响应:{"code":"200","msg":"成功","data":"eyJhbGciOiJIUzUxMiJ9.eyJ1OmxvZ2luOiI6ImFkbWluIn0.a3w5JYZ_FDL0rSQwQzSdcJHGUW8Y0cYxcXKwoNQy9fTsFs6hwzD_869B-lufovN-JecSBuFQNJ_jMG5BiTbIsA"}
* 请求header 添加token参数: Authorization: Bearer token,调用其它接口
*
* @author LGC
*/
@Slf4j
@RequiredArgsConstructor
@RestController
public class UserController {
private final AuthService authService;
@GetMapping("/api/user/login")
public Result<String> login(String username, String password) {
return Result.success(authService.login(username, password));
}
@GetMapping("/api/user/info")
public Result<LoginUser> userIfo() {
log.info("SecurityContextHolder 策略名:{}", SecurityContextHolder.getContextHolderStrategy().getClass().getName());
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return Result.success(loginUser);
}
@RequestMapping("/api/user/index")
public String index() {
return "登陆成功首页index";
}
@PreAuthorize("@ss.hasRole('admin')")
@GetMapping("/api/user/admin")
public String admin() {
return "具有 admin角色 访问成功";
}
@PreAuthorize("@ss.hasPermission('system:user:update')")
@GetMapping("/api/user/update")
public String update() {
return "具有权限key system:user:update 访问成功";
}
@PreAuthorize("@ss.hasPermission('system:user:add')")
@GetMapping("/api/user/add")
public String add() {
return "具有权限key system:user:add 访问成功";
}
@GetMapping("/api/version")
public String version() {
return "v1";
}
}