权限就是菜单(用户管理、订单管理、、、)和按钮(增、删、改、查、、、),还有数据权限就是针对不同的人可以操作不同的数据

权限管理指系统的安全规则及策略,使用户只能访问自己被授权的资源。在权限管理中涉及两个概念:认证、授权。

认证:用户使用账号密码或手机号验证码等方式进行登录,服务端对登录信息匹配认证。

授权:认证成功查询用户权限进项授权。

  1. 权限模型-RBAC0

RBAC基于角色的权限控制。就是在用户和权限之间增加角色概念,为用户分配角色,为角色分配权限,他们之间都是一对多或多对多的关系,用户的权限就是他所有角色的权限集合。

增加角色的好处:

  • 简化权限管理,为多个用户分配角色,只需要更改角色的权限配置就可以更改此角色所有用户的权限
  • 易于维护
  1. 权限模型-RBAC1

在RBAC0的基础上增加了子角色的概念,子角色的权限只能少于父角色的权限不能多于父角色权限。

  1. 权限模型-RBAC2

在RBAC0的基础上增加了对角色的限制,如:

  • 互斥角色 :同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。常举的例子:在审计活动中,一个角色不能同时被指派给会计角色和审计员角色。
  • 基数约束 :一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配。例如公司的领导人有限的;
  • 先决条件角色 :可以分配角色给用户仅当该用户已经是另一角色的成员;对应的可以分配访问权限给角色,仅当该角色已经拥有另一种访问权限。指要想获得较高的权限,要首先拥有低一级的权限。就像我们生活中,国家主席是从副主席中选举的一样。
  • 运行时互斥 :例如,允许一个用户具有两个角色的成员资格,但在运行中不可同时激活这两个角色。
  1. 权限模型-RBAC3

RBAC3称为统一模型,它包含了RBAC1和RBAC2,利用传递性,也把RBAC0包括在内。

Spring Security

Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。

Spring Security流程图

【spring Security】认证授权_自定义Spring Security认证流程分析

1、AbstractAuthenticationProcessingFilter:认证流程的抽象处理器

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {

......

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断需要拦截的请求
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 执行认证逻辑,attemptAuthentication()方法需要在子类中重写
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
// 认证失败
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// 认证失败
unsuccessfulAuthentication(request, response, ex);
}
}

// 认证成功
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {

// 在SecurityContextHolder中设置完成认证的认证信息
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 记住我功能,讲token写入数据库,并且放入cookie中。即使服务器重启也不需要重新登录
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 调用认证成功的处理器
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

// 认证失败
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
// 调用认证失败的处理器
this.failureHandler.onAuthenticationFailure(request, response, failed);
}

......



}


2、UsernamePasswordAuthenticationFilter:用于拦截 /login 的POST请求做认证处理。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private boolean postOnly = true;

......

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 判断请求是否是 POST
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 封装账号密码到 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 调用AuthenticationManager处理认证请求
return this.getAuthenticationManager().authenticate(authRequest);
}
.......

}


3、AuthenticationManager的实现类ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

......

// 针对不同的认证逻辑(微信登录,QQ登录,手机号登录等),都需要去实现AuthenticationProvider
// 处理各自的认真逻辑
private List<AuthenticationProvider> providers = Collections.emptyList();

......

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
// 遍历所有的AuthenticationProvider 匹配当前的认证类型
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
// 调用authenticate方法完成认证逻辑
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}

return result;
}

// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}


4、AuthenticationProvider实现类AbstractUserDetailsAuthenticationProvider:默认的账号密码认证处理器

public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {

// 这里就是账号密码登录的认证逻辑
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// !!!这里是获取spring Security定义的用户信息UserDetails。需要自定义实现类实现UserDetails封装用户信息及权限信息。retrieveUser在子类DaoAuthenticationProvider中
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
...... 省略一些用户信息校验 ......

// 这里返回值的处理也在DaoAuthenticationProvider中
return createSuccessAuthentication(principalToReturn, authentication, user);
}


// 查找对应认证处理器的方法
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}

// 处理返回值---创建一个认证成功的身份证明。这里就是又封装了一下UsernamePasswordAuthenticationToken跟之前封装的区别就是,之前是两个参数的构造,现在是三个参数的构造。看一下UsernamePasswordAuthenticationToken
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}


DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

......

// 这里是交互重点
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// UserDetails 是spring Security定义的用户信息实体,UserDetailsService是spring Security定义的用户服务。我们需要分别实现 UserDetails 和 UserDetailsService中,自定义认证信息及认证逻辑,在UserDetailsService中重写loadUserByUsername(string username)方法完成认证逻辑,可以查询数据库、可以找缓存、可以啥都没有
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// 如果用户信息为空抛异常,认证失败
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

......

// 处理返回值---创建一个认证成功的身份证明。这里主要是可以进行用户的密码修改,然而我没有用过,也不知道这个意义是啥。。。然后又调回了父类的这个方法super.createSuccessAuthentication(),再看回去
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}

}


UsernamePasswordAuthenticationToken

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

......

// 认证前调用的
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}

// 认证成功调用的
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}

// 两个构造函数的区别有两个
// super(authorities); 是设置权限信息
// setAuthenticated(boolean b) 设置权限认证的标志,是否完成认证
......

}


自定义认证逻辑:

  1. 自定义过滤器,继承AuthenticationProvider。定义拦截的请求路径及请求方式,重写attemptAuthentication()方法调用对应的认证逻辑处理器
  2. 自定义Token实体,继承AbstractAuthenticationToken。作为过滤器寻找对应认证处理器的媒介
  3. 自定义认证逻辑的处理器,实现AuthenticationProvider。定义authenticate()方法完成认证逻辑,定义supports()方法用于过滤器与对应处理器的匹配媒介

实现流程大概是这样,具体的细节后面在说。

推荐文章:

RBAC用户、角色、权限、组设计方案 - 左新宇的文章 - 知乎 https://zhuanlan.zhihu.com/p/63769951

SpringSecurity---认证+授权代码实现