文章目录
- SpringSecurity介绍
- SpringSecurity入门
- Spring Security主要jar包功能介绍
- SpringSecurity常用过滤器介绍
- SecurityContextPersistenceFilter
- WebAsyncManagerIntegrationFilter
- HeaderWriterFilter
- CsrfFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- SpringSecurity过滤器链加载原理
- springboot 整合spring-security 搭建工程
- 导入依赖
- 主启动类
- Web控制器
- 配置SpringSecurity
- 认证流程
- 认证流程原理
- 授权流程分析
SpringSecurity介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
SpringSecurity入门
Spring Security主要jar包功能介绍
- spring-security-core.jar:核心包,任何Spring Security功能都需要此包。
- spring-security-web.jar:web工程必备,包含过滤器和相关的Web安全基础结构代码。
- spring-security-config.jar:用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
- spring-security-taglibs.jar:Spring Security提供的动态标签库,jsp页面可以用
spring-boot-starter-security 进行的集成
SpringSecurity常用过滤器介绍
过滤器是一种典型的AOP思想
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。
WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制(仅限于JSP页面)
CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。
LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
当用户以游客身份登录的时候,也就是可以通过设置某些接口可以匿名访问
SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权
限
该过滤器限制哪些资源可以访问,哪些不能够访问
SpringSecurity过滤器链加载原理
通过前面十五个过滤器功能的介绍,它们都是怎么被加载出来的?
DelegatingFilterProxy
入口为springSecurityFilterChain的过滤器DelegatingFilterProxy,
DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器
FilterChainProxy
SecurityFilterChain
最后看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!
springboot 整合spring-security 搭建工程
基于SpringBoot搭建web工程 ,项目名为“spring-security-demo”
导入依赖
项目依赖springboot 核心依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
主启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
Web控制器
当用户认证成功之后会重定向到该方法,返回“登录成功”给用户
@Controller
public class AuthController {
//登录成功后重定向地址
@RequestMapping("/loginSuccess")
@ResponseBody
public String loginSuccess(){
return "登录成功";
}
}
配置SpringSecurity
SpringSecurity提供了一个配置类WebSecurityConfigurerAdapter用来提供给程序员对SpringSecurity做自定义配置,我们需要配置如下几个信息:
- 创建UserDetailService的Bean,该组件是用来加载用户认证信息
- 配置编码器,通过该编码器对密码进行加密匹配。
- 授权规则配置,哪些资源需要什么权限…
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager =
new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
return inMemoryUserDetailsManager;
}
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //授权配置
.antMatchers("/login").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.and().logout().permitAll() //登出路径放行
.and().csrf().disable(); //关闭跨域伪造检查
}
}
浏览器访问:http://localhost:8080/login ,进入Security提供的登录页面,输入账号:zs 密码 123 完成登录,登出成功页面显示 “登录成功”
认证流程
- SpringSecurity根据我们在WebSecurityConfig中的配置会对除了“/login”之外的资源进行拦截做登录检查,
- 如果没有登录会跳转到默认的登录页面“/login” 做登录
- 输入用户名和密码后点击登录,SpringSecurity的拦截器会拦截到登录请求,获取到用户名和密码封装成认证对象(Token对象),底层会调用InMemoryUserDetailsService通过用户名获取用户的认证信息(用户名,密码,权限等,这些信息通常是在数据库存储的)
- 然后执行认证工作:Security把登录请求传入的密码和InMemoryUserDetailsService中加载的用户的密码进行匹配(通过PasswordEncoder), 匹配成功跳转成功地址,认证失败就返回错误
认证流程原理
SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权,SpringSecurity中核心的过滤器链详细如下:
Security相关概念
Authentication
认证对象,用来封装用户的认证信息(账户状态,用户名,密码,权限等)所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码
Authentication常用的实现类:
UsernamePasswordAuthenticationToken:用户名密码登录的Token
AnonymousAuthenticationToken:针对匿名用户的Token
RememberMeAuthenticationToken:记住我功能的的Token
AuthenticationManager
用户认证的管理类,所有的认证请求(比如login)都会通过提交一个封装了到了登录信息的Token对象给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会 调
用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证
信息的Authentication对象。
AuthenticationProvider
认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。
UserDetailService
用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。
SecurityContext
当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的
认证流程原理
- 请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)
- 过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证.
- AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对- - --UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较
- AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回
- Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用- SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后-UsernamePasswordAuthenticationFilter调用- AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作
- 最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到-- SecurityContext对象然后调用SecurityContextRepository将SecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。
注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息。
SecurityContextPersistenceFilter
这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中 获取SecurityContext对象并设置给SecurityContextHolder。在请求完成后将
SecurityContextHolder持有的SecurityContext再保存到配置好的
DecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext
总结一下:SecurityContextPersistenceFilter它的作用就是请求来的时候将包含了认证授权信息的SecurityContext对象从SecurityContextRepository中取出交给SecurityContextHolder工具类,方便我们通过SecurityContextHolder获取SecurityContext从而获取到认证授权信息,请求走的时候又把SecurityContextHolder清空,源码如下:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
...省略...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
...省略部分代码...
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//从SecurityContextRepository获取到SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//把 securityContext设置到SecurityContextHolder,如果没认证通过,这个SecurtyContext就是空的
SecurityContextHolder.setContext(contextBeforeChainExecution);
//调用后面的filter,比如掉用usernamepasswordAuthenticationFilter实现认证
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//如果认证通过了,这里可以从SecurityContextHolder.getContext();中获取到SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
//删除SecurityContextHolder中的SecurityContext
SecurityContextHolder.clearContext();
//把SecurityContext 存储到SecurityContextRepository
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
...省略...
UsernamePasswordAuthenticationFilter
它的重用是,拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用
AuthenticationManager的认证方法进行认证。
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
//从登录请求中获取参数:username,password的名字
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
//默认支持POST登录
private boolean postOnly = true;
//默认拦截/login请求,Post方式
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
// ~ Methods
// ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//判断请求是否是POST
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取到用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//用户名和密码封装Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
//设置details属性
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//调用AuthenticationManager().authenticate进行认证,参数就是Token对象
return this.getAuthenticationManager().authenticate(authRequest);
}
AuthenticationManager
请求通过UsernamePasswordAuthenticationFilter调用AuthenticationManager,默认走的实现类是ProviderManager,它会找到能支持当前认证的AuthenticationProvider实现类调用器authenticate方法执行认证,认证成功后会清除密码,然后抛出AuthenticationSuccessEvent事件
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...省略...
//这里authentication 是封装了登录请求的认证参数,
//即:UsernamePasswordAuthenticationFilter传入的Token对象
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;
boolean debug = logger.isDebugEnabled();
//找到所有的AuthenticationProvider ,选择合适的进行认证
for (AuthenticationProvider provider : getProviders()) {
//是否支持当前认证
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//调用provider执行认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...省略...
}
...省略...
//result就是Authentication ,使用的实现类依然是UsernamepasswordAuthenticationToken,
//封装了认证成功后的用户的认证信息和授权信息
if (result != null) {
if (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 than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
//发布事件
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
DaoAuthenticationProvider
请求到达AuthenticationProvider,默认实现是DaoAuthenticationProvider,它的作用是根据传入的Token中的username调用UserDetailService加载数据库中的认证授权信息(UserDetails),然后使用PasswordEncoder对比用户登录密码是否正确
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//密码编码器
private PasswordEncoder passwordEncoder;
//UserDetailsService ,根据用户名加载UserDetails对象,从数据库加载的认证授权信息
private UserDetailsService userDetailsService;
//认证检查方法
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//获取密码
String presentedPassword = authentication.getCredentials().toString();
//通过passwordEncoder比较密码,presentedPassword是用户传入的密码,userDetails.getPassword()是从数据库加载到的密码
//passwordEncoder编码器不一样比较密码的方式也不一样
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
//检索用户,参数为用户名和Token对象
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//调用UserDetailsService的loadUserByUsername方法,
//根据用户名检索数据库中的用户,封装成UserDetails
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);
}
}
//创建认证成功的认证对象Authentication,使用的实现是UsernamepasswordAuthenticationToken,
//封装了认证成功后的认证信息和授权信息,以及账户的状态等
@Override
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);
}
...省略...
这里提供了三个方法
- additionalAuthenticationChecks:通过passwordEncoder比对密码
- retrieveUser:根据用户名调用UserDetailsService加载用户认证授权信息
- createSuccessAuthentication:登录成功,创建认证对象Authentication
然而你发现 DaoAuthenticationProvider 中并没有authenticate认证方法,真正的认证逻辑是通过父类AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
//认证逻辑
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//得到传入的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//从缓存中得到UserDetails
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//检索用户,底层会调用UserDetailsService加载数据库中的UserDetails对象,保护认证信息和授权信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
...省略...
}
try {
//前置检查,主要检查账户是否锁定,账户是否过期等
preAuthenticationChecks.check(user);
//比对密码在这个方法里面比对的
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
...省略...
}
//后置检查
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//设置UserDetails缓存
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//认证成功,创建Auhentication认证对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
UsernamePasswordAuthenticationFilter
认证成功,请求会重新回到UsernamePasswordAuthenticationFilter,然后会通过其父类AbstractAuthenticationProcessingFilter.successfulAuthentication方法将认证对象封装成SecurityContext设置到SecurityContextHolder中
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//认证成功,吧Authentication 设置到SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
//处理记住我业务逻辑
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//重定向登录成功地址
successHandler.onAuthenticationSuccess(request, response, authResult);
}
授权流程分析
授权一定是在认证通过之后,授权流程是通过FilterSecurityInterceptor拦截器来完成,FilterSecurityInterceptor通过调用SecurityMetadataSource来获取当前访问的资源所需要的权限,然后通过调用AccessDecisionManager投票决定当前用户是否有权限访问当前资源。授权流程如下
-
当客户端向某个资源发起请求,请求到达FilterSecurityInterceptor,然后会调用其父类AbstractSecurityInterceptor
的beforeInvocation方法做授权之前的准备工作 -
在beforeInvocation法中通过SecurityMetadataSource…getAttributes(object);获得资源所需要的访问权限 ,通过SecurityContextHolder.getContext().getAuthentication()获取当前认证用户的认证信息,即包含了认证信息和权限信息的Authentication对象
-
然后FilterSecurityInterceptor通过调用AccessDecisionManager.decide(authenticated, object, attributes);进行授权(authenticated中有用户的权限列表,attributes是资源需要的权限),该方法使用投票器投票来决定用户是否有资源访问权限
-
AccessDecisionManager接口有三个实现类,他们通过通过AccessDecisionVoter投票器完成投票,三种投票策略如下:
- AffirmativeBased : 只需有一个投票赞成即可通过
- ConsensusBased:需要大多数投票赞成即可通过,平票可以配置
- UnanimousBased:需要所有的投票赞成才能通过
而投票器也有很多,如RoleVoter通过角色投票,如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票,AuthenticatedVoter 是用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户(登录后的)
- 投票通过,请求放行,响应对应的资源给客户端
Web授权
web授权API说明
在Security配置类中,可以通过HttpSecurity.authorizeRequests()给资源指定访问的权限,其API如下:
- anyRequest():任何请求
- antMatchers(“/path”) :匹配某个资源路径
- authenticationed() : 保护URL需要登录访问
- permitAll():指定url无需保护(放行)一般用户静态资源
- hasRole(String role):某个资源需要用户拥有什么样的role才能访问
- hasAuthority(String authority):某个资源需要用户拥有什么样的权限才能访问
- hasAnyRole(String …roles):某个资源拥有指定角色中的一个就能访问
- hasAnyAuthority(String … authorities):某个资源拥有指定权限中的一个就能访问
- access(String attribute):该方法使用SPEL表达式,可以创建复杂的限制
- hasIpAddress(String ip):拥有什么样的ip或子网可以访问该资源
授权规则注意
我们通常把细节的规则设置在前面,范围比较大的规则设置放在后面,返例:如有以下配置
.antMatchers("/admin/**").hasAuthority(“admin”)
.antMatchers("/admin/login").permitAll();
那么第二个权限规则将不起作用,因为第一个权限规则覆盖了第二个权限规则
因为权限的设置是按照从上到下的优先级。及满足了最开始的权限设置,那么后面的设置就不起作用了。
Web授权实战
我们这一次在入门案例的基础上进行修改,所有的认证数据,授权数据都从数据库进行获取
1.编写controller
@RestController
public class DeptController {
@RequestMapping("/dept/list")
public String list(){
return "dept.list";
}
@RequestMapping("/dept/add")
public String add(){
return "dept.add";
}
@RequestMapping("/dept/update")
public String update(){
return "dept.update";
}
@RequestMapping("/dept/delete")
public String delete(){
return "dept.delete";
}
}
---------------------------------------------------------
@RestController
public class EmployeeController {
@RequestMapping("/employee/list")
public String list(){
return "employee.list";
}
@RequestMapping("/employee/add")
public String add(){
return "employee.add";
}
@RequestMapping("/employee/update")
public String update(){
return "employee.update";
}
@RequestMapping("/employee/delete")
public String delete(){
return "employee.delete";
}
}
方法上的requestmapping就对应了权限表t_permission的资源
2.配置HttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
List<Permission> permissions = permissionMapper.listPermissions();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
expressionInterceptUrlRegistry = http.csrf().disable() //关闭CSRF跨站点请求伪造防护
.authorizeRequests() //对请求做授权处理
.antMatchers("/login").permitAll() //登录路径放行
.antMatchers("/login.html").permitAll();//对登录页面跳转路径放行
//动态添加授权:从数据库动态查询出,哪些资源需要什么样的权限
for(Permission permission : permissions){
System.out.println(permission.getResource()+" - "+permission.getSn());
//如: /employee/list 需要 employee:list 权限才能访问
expressionInterceptUrlRegistry.antMatchers(permission.getResource()).hasAuthority(permission.getSn());
}
expressionInterceptUrlRegistry
.anyRequest().authenticated() //其他路径都要拦截
.and().formLogin() //允许表单登录, 设置登陆页
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.loginPage("/login.html") //登录页面跳转地址
.loginProcessingUrl("/login") //登录处理地址
.and().logout().permitAll(); //登出
}
解释:上面代码从权限表查询出了所有的资源(对应controller中的Requestmapping路径),然后通过循环调用expressionInterceptUrlRegistry.antMatchers(permission.getResource())
.hasAuthority(permission.getSn()); 进行一一授权,指定哪个资源需要哪个权限才能访问。
3.修改UserDetailService加载用户权限
public UserDetails loadUserByUsername(String username) {
Login loginFromMysql = loginMapper.selectByUsername(username);
if(loginFromMysql == null){
throw new UsernameNotFoundException("无效的用户名");
}
//前台用户
List<GrantedAuthority> permissions = new ArrayList<>();
List<Permission> permissionSnList =
systemManageClient.listByUserId(loginFromMysql.getId());
permissionSnList.forEach(permission->{
System.out.println("用户:"+username+" :加载权限 :"+permission.getSn());
permissions.add(new SimpleGrantedAuthority(permission.getSn()));
});
return new User(username,loginFromMysql.getPassword(),permissions);
}
这里在通过UserDetailServer加载用户认证信息的时候就把用户的权限信息一并加载
方法授权
SpringSecurity提供了一些授权的注解让我们可以在service,controller等的方法上贴注解进行授权,即在方法上指定方法方法需要什么样的权限才能访问
@Secured
标记方法需要有什么样的权限才能访问,这个注解需要在配置类上开启授权注解支持;
@EnableGlobalMethodSecurity(securedEnabled=true) ,然后在Controller方法上贴该注解如:
@Secured(“IS_AUTHENTICATED_ANONYMOUSLY”) :方法可以匿名访问
@Secured(“ROLE_DEPT”) ,需要拥有部门的角色才能访问,ROLE_前缀是固定的
1.开启Secured授权支持
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
2.使用@Secured进行方法授权
@RequestMapping("/employee/list")
@Secured(“ROLE_employee:list”)
public String list(){
return “employee.list”;
}
解释:这里使用了 @Secured(“ROLE_employee:list”) 意思是 “/employee/list” 这个资源需要“ROLE_employee:list”权限才能访问,如果认证的用户有该权限(UserDetailService中加载)包含了“ROLE_employee:list”即可访问该资源,否则不能访问。
注意:对于方法授权,没有贴注解的方法默认是匿名访问。@Secured注解授权是需要加上前缀“ROLE_”
.@PreAuthorize
PreAuthorize适合进入方法前的权限验证,拥有和Secured同样的功能,甚至更强大,该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,然后在Controller贴注解如下:
@PreAuthorize(“isAnonymous()”) : 方法匿名访问
@PreAuthorize(“hasAnyAuthority(‘p_user_list’,‘p_dept_list’)”) :拥有p_user_listr或者p_dept_list的权限能访问
@PreAuthorize(“hasAuthority(‘p_transfer’) and hasAuthority(‘p_read_accout’)”) : 拥有p_transfer权限和p_read_accout权限才能访问.
该标签不需要有固定的前缀。
1.开启@PreAuthorize授权支持
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled= true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
2.使用@PreAuthorize进行方法授权
@PreAuthorize(“hasAnyAuthority(‘employee:add’,‘employee:update’)”)
@RequestMapping("/employee/add")
public String add(){
return “employee.add”;
}
指明了方法必须要有 employee:add 或者 employee:update的权限才能访问 , 该注解不需要有固定的前缀。注意格式“@PreAuthorize(“hasAuthority(‘employee:add’)”)” ,hasAuthority不能省略,括号中是单引号。
3.3.@PostAuthorize
该注解使用并不多,适合在方法执行后再进行权限验证,使用该注解需要在配置类开启:@EanbleGlobalMethodSecurity(prePostEnabled=true) 方法授权支持,用法同 @PreAuthorize一样
到这里授权流程就完成了,这里实现了两种方式的授权,WEB授权和方法授权,WEB授权可以实现统一配置,而方法授权则需要很多的在方法上帖注解,各有各的好处,你个可以根据项目情况自行选择。