【SpringSecurity】初识与集成
个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
前言
之前介绍过Shiro,作为SpringSecurity的精简版,已经具备了大部分常用功能,且更加便于使用,因而一定程度上成为了SpringSecurity的替代品。
相比之下,SpringSecurity功能更加强大完善,通过调整和组合其中的组件,能得到一个高度自定义的安全框架。
本文中,将基于SpringSecurity+Jwt进行学习。
1.介绍
1.1.简介
SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在Spring Boot项目中加入SpringSecurity更是十分简单,使用SpringSecurity 减少了为企业系统安全控制编写大量重复代码的工作。
1.1.1.SpringSecurity主要包括两个目标
- 认证(Authentication):建立一个他声明的主题,即确认用户可以访问当前系统。
- 授权(Authorization):确定一个主体是否允许在你的应用程序执行一个动作,即确定用户在当前系统下所有的功能权限。
Shiro及其他很多安全框架也以此为目标
1.1.2.SpringSecurity的认证模式
在身份验证层,Spring Security 的支持多种认证模式。
这些验证绝大多数都是要么由第三方提供,或由相关的标准组织,如互联网工程任务组开发。
另外Spring Security 提供自己的一组认证功能,内容过长,此处不再赘述,有兴趣自己了解。
1.1.3.核心组件
- SecurityContextHolder:提供对
SecurityContext
的访问 - SecurityContext,:持有Authentication对象和其他可能需要的信息
- AuthenticationManager:其中可以包含多个
AuthenticationProvider
- ProviderManager:对象为
AuthenticationManager
接口的实现类 - AuthenticationProvider:主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
- Authentication:Spring Security方式的认证主体
- GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
- UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
- UserDetailsService:通过username构建
UserDetails
对象,通过loadUserByUsername
根据userName
获取UserDetail
对象
1.1.4.常见过滤器
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- LogoutFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- UsernamePasswordAuthenticationFilter
- BasicAuthenticationFilter
通常可以继承并重写过滤器,已满足业务要求
2.集成
2.1.依赖
// Spring security
implementation 'org.springframework.boot:spring-boot-starter-security:2.3.5.RELEASE'
// JWT
implementation 'com.auth0:java-jwt:3.11.0'
2.2.准备组件
仅列出清单,不做赘述,有兴趣可以直接看demo
- BaseResponse:统一返回结果
- DataResponse:统一返回数据规范
- BaseExceptionHandler:异常统一捕获处理器
2.3.核心组件
2.3.1.配置安全中心(核心)
package com.demo.security;
import com.demo.handler.AuthExceptionHandler;
import com.demo.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description: web安全配置
* @Author: Echo
* @Time: 2020/12/8 11:04
* @Email: 347110596@qq.com
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
/**
* cors跨域配置
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
/**
* @description: WebMvc配置
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 11:12
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 注册cors
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许路径common下的跨域
registry.addMapping("/common/**") // 允许路径
.allowCredentials(true) // 不使用cookie故关闭认证
.allowedOrigins("*") // 允许源,设置为全部
.allowedMethods("*") // 允许方法,设置为全部
.allowedHeaders("*") // 允许头,设置为全部
.maxAge(3600) // 缓存时间,设置为1小时
;
}
}
/**
* @description: SpringSecurity配置
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 10:15
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserService userService;
WebSecurityConfig(UserService userService) {
this.userService = userService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置service,需要实现方法loadUserByUsername,用于登录
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors() // 开启跨域
.and()
.csrf().disable() // 禁用csrf
.antMatcher("/**").authorizeRequests() // 访问拦截
.antMatchers("/auth/**").permitAll() // auth路径下访问放行
.antMatchers("/admin/**").hasRole(AccessConstants.ROLE_ADMIN) // admin路径下限制访问角色
.antMatchers("/user/**").hasRole(AccessConstants.ROLE_USER) // user路径下限制访问角色
.anyRequest().authenticated() // 身份验证
.and()
.addFilter(new JwtTokenFilter(authenticationManager(), userService)) // 自定义过滤器-jwt
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session策略-永不
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthExceptionHandler()) // 异常处理-认证失败
.accessDeniedHandler(new AuthExceptionHandler()); //异常处理-权限错误
}
}
}
核心功能为重写WebSecurityConfigurerAdapter
,设置安全配置
- 重写
configure(AuthenticationManagerBuilder auth)
,设置service,其中service必须继承类UserDetailsService
,并需要重写loadUserByUsername
方法,用于解析token并组装用户信息 - 重写
configure(HttpSecurity http)
,设置相关安全策略,并设置一个过滤器
本配置中还配置了cors
跨域,开放了路径/common/**
下的跨域,请按照实际需求设置
这里还可以设置很多东西,这里只介绍一小部分
2.3.2.自定义token(基于Jwt)
package com.demo.security;
import lombok.Getter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @description: 自定义token
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 10:05
*/
public class JwtToken extends UsernamePasswordAuthenticationToken implements UserDetails {
/**
* token包括的信息
*/
@Getter
private final Type type;
@Getter
private final Long userId;
/**
* token的claim包括的常量key
*/
public static final String CLAIM_KEY_USER_ID = "userId";
public static final String CLAIM_KEY_MOBILE = "mobile";
public static final String CLAIM_KEY_TYPE = "type";
public static final String CLAIM_KEY_RULES = "rules";
public static final int TOKEN_EXPIRES_DAYS = 1;
public JwtToken(Type type, Object principal, Long userId, Collection<? extends GrantedAuthority> authorities) {
// credentials设置为空,身份验证不由jwt管理
super(principal, null, authorities);
this.type = type;
this.userId = userId;
}
// 枚举type
public enum Type {
ADMIN(),
USER(),
;
}
// 弃用密码,不使用
@Override
public String getPassword() {
return null;
}
// 获取账号
@Override
public String getUsername() {
return String.valueOf(this.getPrincipal());
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
继承了类UsernamePasswordAuthenticationToken
并实现接口UserDetails
,用于存储身份信息
重写了从主题中读取账号和密码的功能,因为密码不参与存储故直接返回空,账号则直接类型转换获取
2.3.3.token过滤器
package com.demo.security;
import com.google.common.base.Strings;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* @description: token过滤器,提供给SpringSecurity使用
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 10:07
*/
public class JwtTokenFilter extends BasicAuthenticationFilter {
public static final String TOKEN_HEADER = "Token";
public static final String TOKEN_SECRET = "e8258f17-b436-4cad-bfc3-2d810ec86238";
private final UserDetailsService userDetailsService;
public JwtTokenFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
super(authenticationManager);
this.userDetailsService = userDetailsService;
}
/**
* AOP,将token中的数据,提取出来放入request作为参数
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader(TOKEN_HEADER);
if (!Strings.isNullOrEmpty(token)) {
JwtToken details = (JwtToken) userDetailsService.loadUserByUsername(token);
if (Objects.nonNull(details)) {
SecurityContextHolder.getContext().setAuthentication(details);
request.setAttribute("authId", details.getUserId());
request.setAttribute("authType", details.getType());
request.setAttribute("auth", details);
}
}
chain.doFilter(request, response);
}
}
需要继承类BasicAuthenticationFilter
,并重写其内部过滤方法,通过AOP
的方式将token中的数据写入request
之前介绍的纯Jwt认证框架也是使用的AOP来校验token参数和写入参数到request
2.3.4.认证异常handler
package com.demo.handler;
import com.demo.response.BaseResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @description: 认证异常handler
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 9:40
*/
@Slf4j
public class AuthExceptionHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
private final ObjectWriter objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter();
/**
* 禁止访问
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
sendError(response, false, accessDeniedException.getLocalizedMessage());
}
/**
* 未登录
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
sendError(response, true, authException.getLocalizedMessage());
}
private void sendError(HttpServletResponse response, boolean redirectLogin, String message) throws IOException {
response.setCharacterEncoding(Charsets.UTF_8.displayName());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
log.error("权限错误", message);
objectWriter.writeValue(response.getWriter(), redirectLogin ? BaseResponse.RESPONSE_NOT_LOGIN : BaseResponse.RESPONSE_AUTH_DENIED);
}
}
其实就是同时继承权限拒绝回调和认证拒绝回调,进行统一处理,分开写也是可以的,写一起只是为了省事。。。
2.3.5.权限鉴别器(核心)
package com.demo.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
/**
* @description: 权限鉴别器
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 9:28
*/
@Slf4j
@Component
public class GlobalPermissionEvaluator implements PermissionEvaluator {
/**
* 自定义断言
*/
public interface EvalPredicate extends BiPredicate<Object, JwtToken> {
}
/**
* 权限map,用于存储相关权限
*/
private final Map<Object, EvalPredicate> userPredicates = new HashMap<>();
private final Map<Object, EvalPredicate> adminPredicates = new HashMap<>();
/**
* @description: 检查权限
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 9:29
*/
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (!(authentication instanceof JwtToken)) {
return false;
}
// 转换为可识别的 userDetails
JwtToken userDetails = (JwtToken) authentication;
Map<Object, EvalPredicate> predicates = getPredicates(userDetails.getType());
// 检查权限
boolean pass = false;
if (predicates.containsKey(permission)) {
pass = predicates.get(permission).test(targetDomainObject, userDetails);
} else {
log.info("reject permission: {} {}", permission, targetDomainObject);
}
return pass;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return hasPermission(authentication, targetId, permission);
}
/**
* @description: 权限注册
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 9:38
*/
public void registerPermission(JwtToken.Type type, Object permission, EvalPredicate predicate) {
Map<Object, EvalPredicate> predicates = getPredicates(type);
if (predicates.containsKey(permission)) {
throw new DuplicateKeyException("Permission handler duplicate");
}
predicates.put(permission, predicate);
}
/**
* @description: 获取权限
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 9:38
*/
private Map<Object, EvalPredicate> getPredicates(JwtToken.Type type) {
// 获取集合
Map<Object, EvalPredicate> predicates;
switch (type) {
case USER -> predicates = this.userPredicates;
case ADMIN -> predicates = this.adminPredicates;
default -> throw new SecurityException("Permission type not supported!");
}
return predicates;
}
}
需要实现接口PermissionEvaluator
,实现其权限认证逻辑
采用方案如下
- 鉴别器中定义map,用于存储每个类型的权限和对应的断言
- service中注册时,将所需要的注册的权限类型和对应的断言,注册到鉴别器中
- 认证权限时,会调用相对应的断言,进行权限测试
2.3.6.service层
package com.demo.service;
import com.demo.security.GlobalPermissionEvaluator;
/**
* @Description:权限service接口层
* @Author: Echo
* @Time: 2020/12/8 16:55
* @Email: 347110596@qq.com
*/
public interface AccessService {
/**
* 注册权限信息
*/
void initEvaluator(GlobalPermissionEvaluator evaluator);
/**
* 校验权限信息
*/
boolean isAccessible(Long userId, Long targetId);
}
package com.demo.service;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @description: 用户service接口层
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 10:16
*/
public interface UserService{
/**
* 登录
*
* @param username 账号
* @param password 密码
* @return token
*/
String login(String username, String password);
}
package com.demo.service.impl;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.security.AccessConstants;
import com.demo.security.GlobalPermissionEvaluator;
import com.demo.security.JwtToken;
import com.demo.security.JwtTokenFilter;
import com.demo.service.AccessService;
import com.demo.service.UserService;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @description: 用户service实现层
* @author: Echo
* @email: echo_yezi@qq.com
* @time: 2020/12/8 10:16
*/
@Service
public class UserServiceImpl implements UserService, AccessService {
private final Algorithm jwtAlgorithm;
private final JWTVerifier jwtVerifier;
public UserServiceImpl(GlobalPermissionEvaluator evaluator) {
this.jwtAlgorithm = Algorithm.HMAC256(JwtTokenFilter.TOKEN_SECRET.getBytes());
this.jwtVerifier = JWT.require(jwtAlgorithm).build();
initEvaluator(evaluator);
}
/**
* 注册权限信息
*/
@Override
public void initEvaluator(GlobalPermissionEvaluator evaluator) {
// todo 按照实际情况注册权限
// 注册管理员的更新权限
evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
this.isAccessible(token.getUserId(), (Long) targetId));
// 注册用户的更新权限
evaluator.registerPermission(JwtToken.Type.USER, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
this.isAccessible(token.getUserId(), (Long) targetId));
}
/**
* 校验权限信息
*/
@Override
public boolean isAccessible(Long userId, Long targetId) {
// todo 从数据库查询校验权限
Set<Long> accessIds = switch (userId.toString()) {
case "1" -> Sets.newHashSet(1L, 2L, 3L, 4L);
case "2" -> Sets.newHashSet(1L, 2L, 3L);
case "3" -> Sets.newHashSet(1L, 2L);
case "4" -> Sets.newHashSet(1L);
default -> Sets.newHashSet();
};
return accessIds.contains(targetId);
}
/**
* 装载token
*/
@Override
public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException {
try {
//解析token
DecodedJWT verify = jwtVerifier.verify(token);
Long userId = verify.getClaim(JwtToken.CLAIM_KEY_USER_ID).asLong();
String mobile = verify.getClaim(JwtToken.CLAIM_KEY_MOBILE).asString();
JwtToken.Type type = verify.getClaim(JwtToken.CLAIM_KEY_TYPE).as(JwtToken.Type.class);
String rules = Strings.nullToEmpty(verify.getClaim(JwtToken.CLAIM_KEY_RULES).asString());
Preconditions.checkNotNull(userId);
Preconditions.checkState(!Strings.isNullOrEmpty(mobile));
// 获取权限
//noinspection UnstableApiUsage
Set<GrantedAuthority> authorities = Splitter.on("|")
.splitToStream(rules)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
//组装token todo这里
return new JwtToken(type, mobile, userId, authorities);
} catch (Throwable e) {
throw new UsernameNotFoundException(e.getMessage());
}
}
/**
* 登录
*
* @param username 账号
* @param password 密码
* @return token
*/
@Override
public String login(String username, String password) {
// 相关参数 todo 正常业务中需要从数据库中查询,这里也未校验密码
Long userId = null;
String mobile = null;
String rules = null;
JwtToken.Type type = null;
switch (username) {
case "111":
userId = 1L;
mobile = "13411111111";
rules = AccessConstants.formatAccess(
AccessConstants.ACCESS_FIND,
AccessConstants.ACCESS_UPDATE,
AccessConstants.ACCESS_DELETE,
AccessConstants.ACCESS_INSERT,
AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
);
type = JwtToken.Type.ADMIN;
break;
case "222":
userId = 2L;
mobile = "13422222222";
rules = AccessConstants.formatAccess(
AccessConstants.ACCESS_FIND,
AccessConstants.ACCESS_UPDATE,
AccessConstants.ROLE_ADMIN,
AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
);
type = JwtToken.Type.ADMIN;
break;
case "333":
userId = 3L;
mobile = "13433333333";
rules = AccessConstants.formatAccess(
AccessConstants.ACCESS_FIND,
AccessConstants.ACCESS_INSERT,
AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
);
type = JwtToken.Type.USER;
break;
case "444":
userId = 4L;
mobile = "1344444444";
rules = AccessConstants.formatAccess(
AccessConstants.ACCESS_FIND,
AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
);
type = JwtToken.Type.USER;
break;
default:
throw new RuntimeException("账号或密码不正确");
}
// 过期时间为1天
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DATE, JwtToken.TOKEN_EXPIRES_DAYS);
//组装token
return JWT.create()
.withClaim(JwtToken.CLAIM_KEY_USER_ID, userId)
.withClaim(JwtToken.CLAIM_KEY_MOBILE, mobile)
.withClaim(JwtToken.CLAIM_KEY_RULES, rules)
.withClaim(JwtToken.CLAIM_KEY_TYPE, type.toString())
.withIssuer(this.getClass().getSimpleName())
.withIssuedAt(new Date())
.withExpiresAt(calendar.getTime())
.sign(jwtAlgorithm);
}
}
分三个功能:
-
实现登录逻辑供controller层接口使用,通过账号密码查询相关信息并生成token
为作为controller调用的业务层负责的内容,与框架本身无关
-
实现
loadUserByUsername(String token)
方法供安全中心使用,通过token解析出相关数据,并转换为UserDetails
对象为安全中心需要的功能,如果有多个身份验证方法也可以多个类实现该方法,并在安全中心选择合适的进行配置
-
实现注册权限信息方法和权限校验方法,并在初始化的时候进行注册,权限校验(hasPermission)时调用校验
为权限校验相关方法,service初始化时进行注册权限并存储校验方法,所有的权限都应该在合适的地方注册并存储权限校验方法
另一个service
package com.demo.service.impl;
import com.demo.security.AccessConstants;
import com.demo.security.GlobalPermissionEvaluator;
import com.demo.security.JwtToken;
import com.demo.service.AccessService;
import com.google.common.collect.Sets;
import org.springframework.stereotype.Service;
import java.util.Set;
/**
* @Description:测试service,用于测试其他权限
* @Author: Echo
* @Time: 2020/12/8 17:50
* @Email: 347110596@qq.com
*/
@Service
public class TestServiceImpl implements AccessService {
public TestServiceImpl(GlobalPermissionEvaluator evaluator) {
initEvaluator(evaluator);
}
@Override
public void initEvaluator(GlobalPermissionEvaluator evaluator) {
// 注册管理员的详情权限
evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_DETAIL, (targetId, token) ->
this.isAccessible(token.getUserId(), (Long) targetId));
}
@Override
public boolean isAccessible(Long userId, Long targetId) {
// todo 从数据库查询校验权限
Set<Long> accessIds = switch (userId.toString()) {
case "1" -> Sets.newHashSet(1L, 2L);
case "2" -> Sets.newHashSet(1L);
default -> Sets.newHashSet();
};
return accessIds.contains(targetId);
}
}
2.4.测试controller
2.4.1.登录
package com.demo.controller;
import com.demo.service.UserService;
import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController("authController")
@RequestMapping("/auth")
public class LoginController {
private final UserService userService;
public LoginController(UserService userService) {
this.userService = userService;
}
@PostMapping("login")
public DataResponse<?> login(
@RequestParam(name = "username") String username,
@RequestParam(name = "password") String password) {
return new DataResponse(userService.login(username, password));
}
}
运行结果:
2.4.2.admin权限
package com.demo.controller;
import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController("adminTestController")
@RequestMapping("/admin/test")
public class AdminTestController {
/**
* 测试update权限
*/
@GetMapping("checkLogin")
@PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
public DataResponse<?> checkLogin() {
return new DataResponse("OK");
}
/**
* 测试对目标的update权限
*/
@GetMapping("checkUpdatePermission")
@PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)"
+ "&&hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
public DataResponse<?> checkUpdatePermission(Long targetId) {
return new DataResponse("OK");
}
/**
* 测试对目标的detail权限
*/
@GetMapping("checkDetailPermission")
@PreAuthorize("hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_DETAIL)")
public DataResponse<?> checkDetailPermission(Long targetId) {
return new DataResponse("OK");
}
}
运行结果:
token写入headers里面,管理员账号均为111
2.4.3.user权限
package com.demo.controller;
import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController("userTestController")
@RequestMapping("/user/test")
public class UserTestController {
/**
* 检查登录状态
*/
@GetMapping("checkLogin")
public DataResponse<?> checkLogin() {
return new DataResponse("OK");
}
/**
* 检查update权限
*/
@GetMapping("checkPermission")
@PreAuthorize("hasUpdatePermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
public DataResponse<?> checkUpdatePermission(Long targetId) {
return new DataResponse("OK");
}
}
使用之前账号为111的token访问(admin账号)
换用账号为444的token访问
测试结果均符合预期
完整demo地址:https://gitee.com/echo_ye/spring-security-demo
3.小结
SpringSecurity
主要是用注解、aop、重写组件等方法,来对框架进行自定义
由于SpringSecurity
是spring
生态中重要的一员,不断随着版本更新维护而越来越完善和强大
SpringSecurity
提供了安全策略设置,进而对全局的请求进行拦截和过滤,保证项目的安全性
SpringSecurity
也可以设置诸如cors、crsf等,可以自定义,但默认关闭,需要开启否则会屏蔽设置给springMVC的相同的设置
4.SpringSecurity与shiro、jwt
SpringSecurity
与shiro
和jwt
相比之下,更显得完善和强大,一方面能够给开发者更大的自由发挥能力,开发出更符合业务需求的安全框架,但另一方面也略显臃肿
shiro
相当于SpringSecurity
的精简版,基本沿用了主体结构,并加以精简和优化,使得使用起来更加方便
jwt
由于其特性,更加轻便和简洁,但能力也更弱,单由jwt
只能实现简单的权限校验,不适合用于较大的框架(能力不够&安全不够),因此往往与shiro
、SpringSecurity
等框架进行组合,共同协作来进行优势互补
后记
相比之下。。我更愿意用shiro摆平一切。。
作者:Echo_Ye
WX:Echo_YeZ
Email :echo_yezi@qq.com
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)