背景
spring家族的安全框架,两大核心: 认证(Authentication)和授权(Authorization) 。
通俗说:认证—你是谁?
授权----你有什么权限?
在认证和授权这两个核心功能之外,SpringSecurity还提供了很多安全管理的“周边功能”,这也是一个非常重要的特色,在开发中,即便我们不了解很多网络攻击,只要用了SpringSecurity它会帮助我们自动防御很多网络攻击,例如CSRF攻击,会话固定攻击等,同时还提供了HTTP防火墙来拦截大量的非法请求。
认证
认证规则有很多,有:
- 表单认证
- OAuth2.0认证
- SAML2.0认证
- CAS认证
- RememberMe自动认证
- JAAS认证
- OpenID认证
- Pre-Authentication Scenarios认证
- X509认证
- HTTP Basic认证
- HTTP Digest认证
- 自定义认证等等
看一下 Authentication 认证接口:
public interface Authentication extends Principal, Serializable {
//用来获取用户的权限
Collection<? extends GrantedAuthority> getAuthorities();
//用来获取用户凭证,一般来说是密码
Object getCredentials();
//用来获取用户携带的详细信息,可能是当前请求之类等
Object getDetails();
//用来获取当前用户,例如是一个用户或一个用户对象
Object getPrincipal();
//当前用户是否认证成功。
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
管理认证的接口 AuthenticationManager :
/**
AuthenticationManager接口只有一个authenticate方法可以用来做认证,该方法有三个不同的返回值:
返回Authentication : 表示认证成功
抛出AuthenticationException异常:表示用户输入了无效的凭证.
返回null,表示不能断定
*/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
实现认证管理的类 ProviderManager :
/**
AuthenticationManager最主要的实现类是ProviderManager,ProviderManager管理了众多的AuthenticationProvider实例,AuthenticationProvider有点类似于AuthenticationManager,但是它多了一个supports方法用来短评是否指出给定的Authenticaion类型。
*/
public class ProviderManager implements AuthenticationManager{
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
}
}
具体管理某个认证的接口 AuthenticationProvider
AuthenticationProvider类似于AuthenticationManager ,只不过其只针对一个认证
/**
由于Authentication拥有众多不同的实现类,这些不同的实现类又由不能的AuthenticationProvider来处理,所以AuthenticationProvider会有一个supports方法,来判断当前的AuthenticationProvider是否支持对应的Authentication
*/
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
授权
- 基于URL的请求授权
- 支持方法的访问授权
- 支持SpEL访问控制
- 支持域对象安全
- 同时也支持动态权限配置
- 支持RBAC权限模型等
认证相关接口:
投票 接口AccessDecisionVoter
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
管理投票接口 AccessDecisionManager
AccessDecisionVoter和AccessDecisionManager都有众多的实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问
public interface AccessDecisionManager {
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
过滤器
我们所见到SpringSecurity提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终行程一个过滤器链。我们也可以自定义过滤器,并通过@Order来调整自定义过滤器在过滤链中的位置。
需要注意的是,默认过滤器并不是直接放在Web项目的原生过滤器链中,而是通过一个FilterChainProxy来统一管理。SpringSecurity中的过滤链通过FilterChainProxy嵌入到Web项目的原生过滤器链中,如下:
具体案例
springsecurity安全额配置类
/**
* Spring security web security config.
*
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
public static final String API_PATTERN = "/api/**";
/** 无需认证的API patterns */
public static String[] PUBLIC_API_PATTERNS =
new String[] {
"/api/users/login",
....
};
/** 需要认证的API patterns */
public static String[] PRIVATE_API_PATTERNS = new String[] {API_PATTERN, "/api2/**"};
/**X-Frame-Options设置
deny:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
sameorigin:表示该页面可以在相同域名页面的 frame 中展示。
allow-from: 这是一个被弃用的指令,不再适用于现代浏览器;意思是只能被嵌入到指定域名的框架中
**/
public static final String X_FRAME_OPT_DENY = "deny";
public static final String X_FRAME_OPT_SAME_ORIGIN = "sameorigin";
public static final String X_FRAME_OPT_ALLOW_FROM_PREFIX = "allow-from ";
@Setter(onMethod_ = {@Autowired})
private AccessTokenAuthenticationProvider accessTokenAuthProvider;
@Setter(onMethod_ = {@Autowired})
private SettingService settingService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 设置认证
AccessTokenAuthenticationFilter accessTokenAuthenticationFilter =
new AccessTokenAuthenticationFilter();
http.authorizeRequests()
// 公共API无需认证
.antMatchers(PUBLIC_API_PATTERNS)
.permitAll()
.antMatchers(PRIVATE_API_PATTERNS)
.authenticated()
.and()
// 提取令牌
.addFilterBefore(accessTokenAuthenticationFilter, BasicAuthenticationFilter.class)
// 验证令牌
.authenticationProvider(this.accessTokenAuthProvider)
.exceptionHandling()
// 对于认证失败的返回401错误,而不是默认的403错误,请见:https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.html#authenticationEntryPoint(org.springframework.security.web.AuthenticationEntryPoint)。
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.headers()
// 设置X-Frame-Options响应头
//避免自己的网页被嵌入到别的站点
.frameOptions(
options -> {
String opt = 数据库或配置文件读取设置;
if (StringUtils.isBlank(opt)) {
options.disable();
} else if (X_FRAME_OPT_SAME_ORIGIN.equalsIgnoreCase(opt)) {
options.sameOrigin();
} else if (StringUtils.startsWithIgnoreCase(opt, X_FRAME_OPT_ALLOW_FROM_PREFIX)) {
options
.disable()
.addHeaderWriter(
new StaticHeadersWriter(
XFrameOptionsHeaderWriter.XFRAME_OPTIONS_HEADER, opt));
} else {
// Default policy.
options.deny();
}
});
// CSRF设置,将token存在cookie中,前端需要把cookie中的token读出来放到请求header里,必须要httpOnly的cookie才能被js读取
boolean csrfCheckEnabled = 数据库或者配置文查找是否支持跨域;
List<String> csrfCheckHttpMethods =
settingService.getStringList(SettingItem.WEB_CSRF_CHECK_HTTP_METHODS);
List<String> csrfCheckExcludePaths =
settingService.getStringList(SettingItem.WEB_CSRF_CHECK_EXCLUDE_PATHS);
if (csrfCheckEnabled) {
http.csrf()
.requireCsrfProtectionMatcher(
request -> {
AntPathMatcher antPathMatcher = new AntPathMatcher();
String path = request.getRequestURI();
String method = request.getMethod();
// 检查白名单
if (Seq.seq(csrfCheckExcludePaths)
.anyMatch(exclude -> antPathMatcher.match(exclude, path))) {
return false;
}
// 仅仅对API启用csrf
if (!antPathMatcher.match(API_PATTERN, path)) {
return false;
}
if (Seq.seq(csrfCheckHttpMethods)
.noneMatch(checkMethod -> checkMethod.equalsIgnoreCase(method))) {
return false;
}
return true;
})
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
} else {
http.csrf().disable();
}
return http.build();
}
/**
提取令牌
*/
public static class AccessTokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain)
throws ServletException, IOException {
AntPathMatcher antPathMatcher = new AntPathMatcher();
String path = request.getRequestURI();
// 公共API无需认证,非私用API无需认证
boolean skipAuth =
Seq.of(PUBLIC_API_PATTERNS).anyMatch(pattern -> antPathMatcher.match(pattern, path))
|| Seq.of(PRIVATE_API_PATTERNS)
.noneMatch(pattern -> antPathMatcher.match(pattern, path));
if (skipAuth) {
// 如果无需认证但是Authentication却被设置了,需要清空,否则还是会进行认证,这可能发生在SecurityContextPersistenceFilter从
// session中恢复出了一个session的认证信息时。
if (SecurityContextHolder.getContext().getAuthentication()
instanceof AccessTokenAuthentication) {
SecurityContextHolder.getContext().setAuthentication(null);
}
} else {
Optional<String> userNameOpt = UserService.getAuthUserName(request);
Optional<String> tokenOpt = UserService.getAuthToken(request);
String clientIp = WebServerUtils.getClientIp(request);
AccessTokenAuthentication auth =
new AccessTokenAuthentication(
tokenOpt.orElse(null), userNameOpt.orElse(null), clientIp);
// 将authentication设置到(*****)SecurityContext(******),下面会取出来放到userContext方便随时读取用户信息
SecurityContextHolder.getContext().setAuthentication(auth);
}
// 返回这个链式调用
chain.doFilter(request, response);
}
}
//设置authentication中包含的信息
public static class AccessTokenAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = 7495641216462496304L;
private final String accessToken;
private final String userName;
private final String clientIp;
public AccessTokenAuthentication(String accessToken, String userName, String clientIp) {
super(null);
this.accessToken = accessToken;
this.userName = userName;
this.clientIp = clientIp;
setAuthenticated(false);
}
public String getAccessToken() {
return this.accessToken;
}
public String getUserName() {
return this.userName;
}
public String getClientIp() {
return this.clientIp;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
}
/** 验证令牌 */
@Component
@AllArgsConstructor(onConstructor_ = {@Autowired})
public static class AccessTokenAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
AccessTokenAuthentication tokenAuth = (AccessTokenAuthentication) authentication;
//其实就是从缓存中取用户的登录信息
userService.authenticate(tokenAuth.userName, tokenAuth.accessToken, tokenAuth.clientIp);
authentication.setAuthenticated(true);
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return AccessTokenAuthentication.class.isAssignableFrom(authentication);
}
}
@Bean
public HttpFirewall httpFirewall() {
// APT的客户端会发出路径包含"/./"的请求,默认的StrictHttpFirewall认为这是非法的url,会拒绝请求,换用DefaultHttpFirewall允许这种请求。
return new DefaultHttpFirewall();
}
}
csrf:
配置登录缓存和全局信息
@Configuration
public class UserConfig {
/**
用户登录成功后将一些登录信息放入里面(比如用户名:登录信息【ip、token、权限】)
loginCache.put(userName, new LoginInfo(clientIp, token, authPerms))
*/
@Bean
public Cache<String, LoginInfo> loginCache() {
return Caffeine.newBuilder()
.expireAfterWrite(从数据库或者配置文件中读取过期时间)
.build();
}
//将用户的登录信息
@Bean
@RequestScope
public UserContext userContext(
HttpServletRequest request,
Cache<String, LoginInfo> loginCache,
PreferenceService prefService) {
UserContext context = new UserContext();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof WebSecurityConfig.AccessTokenAuthentication) {
// 带有令牌
WebSecurityConfig.AccessTokenAuthentication tokenAuth =
(WebSecurityConfig.AccessTokenAuthentication) auth;
context.setIpAddr(tokenAuth.getClientIp());
context.setUserName(tokenAuth.getUserName());
} else {
// 没有令牌
context.setIpAddr(WebServerUtils.getClientIp(request));
context.setUserName("");
}
LoginInfo loginInfo = loginCache.getIfPresent(context.getUserName());
if (loginInfo != null) {
// Request from logged in user.
context.setPerms(loginInfo.getPerms());
}
context.setLocale(I18nService.detectLocale(request));
return context;
}
}
方法级的授权
当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使用 @EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能。
prePostEnabled
prePostEnabled = true 会解锁 @PreAuthorize / @PostAuthorize/ @PreFilter/@PostFilter
- 字就可以看出@PreAuthorize 注解会在方法执行前进行验证
- @PostAuthorize 注解会在方法执行后进行验证。
- @PreFilter: 对集合类型的参数执行过滤,移除结果为false的元素。
- @PostFilter 基于返回值相关的表达式,对返回值进行过滤
Secured
@Secured注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。 但是其不支持springrl表达式。
jsr250E
启用 JSR-250 安全控制注解,这属于 JavaEE 的安全规范(现为 jakarta 项目)。一共有五个安全注解。如果你在 @EnableGlobalMethodSecurity 设置 jsr250Enabled 为 true ,就开启了 JavaEE 安全注解中的以下三个
- @DenyAll: 拒绝所有访问
- @RolesAllowed({“USER”, “ADMIN”}): 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
- @PermitAll: 允许所有访问
/**
* Spring security method security config.
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
private CustomPermissionEvaluator customPermissionEvaluator;
@Autowired
@Lazy
public void setCustomPermissionEvaluator(CustomPermissionEvaluator customPermissionEvaluator) {
this.customPermissionEvaluator = customPermissionEvaluator;
}
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(this.customPermissionEvaluator);
return handler;
}
@Component
@AllArgsConstructor(onConstructor_ = {@Autowired})
public static class CustomPermissionEvaluator implements PermissionEvaluator {
/**
* Return true if the user has permission to access the target, false otherwise.
*/
@Override
public boolean hasPermission(
Authentication authentication, Object targetDomainObject, Object permission) {
return false;
}
/**
* Return true if the user has permission to access the target, false otherwise.
*/
@Override
@SuppressWarnings({"rawtypes"})
public boolean hasPermission(
Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
//将所需的权限 放在set中
Set<Access> accesses = new HashSet<>();
if (targetId instanceof Collection) {
for (Object singleId : (Collection) targetId) {
accesses.add(toAccess((Long) singleId, targetType, permission));
}
} else {
accesses.add(toAccess((Long) targetId, targetType, permission));
}
//和用户自身的权限比对 查看是否有权限
return hasPermission(accesses);
}
private Access toAccess(Long id, String targetType, Object permission) {
return new Access(
new Access.Target(Access.TargetType.valueOf(targetType), id),
Access.Action.valueOf((String) permission));
}
private boolean hasPermission(Set<Access> accesses) {
Set<AuthPerm> required = new HashSet<>();
for (Access access : accesses) {
switch (access.getTarget().getType()) {
case Type1:
required.addAll();
break;
...
default:
break;
}
}
//和用户所具有的的权限(即从userContext【根本上是从数据库里】中取 )比较,看用户是否有这个权限
return permService.hasPerms(required);
}
}
}
接口上使用:
@GetMapping("/get")
@PreAuthorize("hasPermission(#resourceId,'OPERATION', 'VIEW')")
public List<Resurce> getResource(@RequestParam(value = "resourceId") Long resourceId){
}
权限类设置
import lombok.Value;
@Value
public class Access {
//针对什么目标
Target target;
//对这个目标有什么权限
Action action;
@Value
public static class Target {
TargetType type;
Long id;
}
public enum TargetType {
USER,
BOOK,
PERMISSION,
OPERATION
}
public enum Action {
CREATE,
EDIT,
DELETE,
VIEW,
DOWNLOAD,
}
}
数据库权限类
@Table("role_perms")
@Data
@AllArgsConstructor
public class Perm {
@Id
@Column("roleName")
private String roleName;
@Column("perm")
private PermType perm;
@Column("resId")
private long resId;
}
@AllArgsConstructor
@Getter
public enum PermType {
ADMIN(false, AccessLevel.ADMIN),
GUEST(false, AccessLevel.GUEST),
USER_ADMIN(true, AccessLevel.ADMIN),
USER_GUEST(true, AccessLevel.GUEST),
BOOK_ADMIN(true, AccessLevel.ADMIN),
;
private final boolean requireResId;
private final AccessLevel accessLevel;
}
public enum AccessLevel {
/** ADMIN level. */
ADMIN,
/** GUEST level. */
GUEST
}