Spring Boot整合Spring Security实现权限控制
学习Spring Security过后进行总结和梳理,首先说一下关于认证授权的基本概念。
基本概念
什么是认证?
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证 相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码 登录微信的过程就是认证。
系统为什么要认证
?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。
认证
:用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录*,*指纹认证等方式。
什么是会话?
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
基于session的认证方式如下:
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于token方式如下:
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie。
基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代,更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
什么是授权?
比如:微信登录成功后用户即可使用微信的功能,如:发红包、发朋友圈、添加好友等,如未绑定银行卡用户则无法发送红包,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源
,用户拥有发红包功能的权限才可以正常使用发送红包功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
特征
- 对身份验证和授权的全面和可扩展的支持
- 防止会话固定,点击劫持,跨站点请求伪造等攻击
- Servlet API集成
- 可选与Spring Web MVC集成
- Much more
下面我们来实现一个简单认证授权的demo,首先引入pom.xml
<!-- 以下是>spring boot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 以下是>spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Boot热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
编写SpringSecurity
的配置类,该类需要继承WebSecurityConfigurerAdapter
spring security提供的用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p2")
.antMatchers("/r/r2").hasAuthority("p3")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
.and()
//开启自动配置的登陆功能,如果没有登录,则会来到登录页面
.formLogin()//允许表单登录
//.loginPage("/login-view")//自定义登录页面
.loginProcessingUrl("/login")
.successForwardUrl("/login-success")
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)//自定义登录成功的页面地址
.and()
.logout()
.logoutUrl("/logout")//访问/logout表示用户注销,清空session
.logoutSuccessUrl("/");//注销以后来到首页
}
}
自定义UserDetailsService
@Service
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired
private UserDao userDao;
//根据 账号查询用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//将来连接数据库根据账号查询用户信息
TUser user = userDao.getUserByUsername(username);
if (user == null) {
//如果用户查不到,返回null,由provider来抛出异常
return null;
}
// 根据用户的id查询用户的权限
List<String> permissions = userDao.findPermissionsByUserId(user.getId());
// //将permissions转成数组
String[] permissionArray = new String[permissions.size()];
permissions.toArray(permissionArray);
UserDetails userDetails = User.withUsername(user.getUsername()).password(user.getPassword()).authorities(permissionArray).build();
return userDetails;
}
}
UserDetailsService 接口只有一个方法,loadUserByUsername(String username),一般需要我们实现此接口方法,根据用户名加载登录认证和访问授权所需要的信息,并返回一个 UserDetails的实现类,后面登录认证和访问授权都需要用到此中的信息。
UserDetails 提供了一个默认实现 User,主要包含用户名(username)、密码(password)、权限(authorities)和一些账号或密码状态的标识。如果默认实现满足不了你的需求,可以根据需求定制自己的 UserDetails,然后在 UserDetailsService 的 loadUserByUsername 中返回即可。
LoginController内容
@RestController
public class LoginController {
@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
//提示具体用户名称登录成功
return getUsername()+" 登录成功";
}
/**
* 测试资源1
* @return
*/
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
// 要使用注解限制方法访问权限需要开启WebSecurityConfig中@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
// @PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问,方法调用的权限控制。
public String r1(){
return getUsername()+" 访问资源1";
}
/**
* 测试资源2
* @return
*/
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
// @PreAuthorize("hasAnyAuthority('p1','p3')")//拥有p2权限才可以访问
// @PreAuthorize("hasAuthority('p2')")//拥有p1权限才可以访问
public String r2(){
return getUsername()+" 访问资源2";
}
//获取当前用户信息
private String getUsername(){
String username = null;
//当前认证通过的用户身份
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//用户身份
Object principal = authentication.getPrincipal();
if(principal == null){
username = "匿名";
}
if(principal instanceof UserDetails){
UserDetails userDetails = (UserDetails) principal;
username = userDetails.getUsername();
}else{
username = principal.toString();
}
return username;
}
}
工作原理
1、结构总览
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,SpringSecurity对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为
org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器
(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
-
SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截
器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给
SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext; -
UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 -
AuthenticationFailureHandler
,这些都可以根据需求做相关改变; -
FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了; -
ExceptionTranslationFilter
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
2、认证流程
让我们仔细分析认证过程:
- 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,
封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。 - 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个
List 列表,存放多种认证方式,最终实际的认证工作是由
AuthenticationProvider 完成的。咱们知道web表单的对应的AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService 负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
我们来看一下Authentication(认证信息)的结构,它是一个接口
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
- Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security
包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。 - getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
- getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
- getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
-
getPrincipal()
,身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
3、授权流程
Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。SpringSecurity使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security的授权流程如下:
分析授权流程:
-
拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子
类拦截。 -
获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限
Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:http .authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2")
-
最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问
最终效果:
1、测试认证
2、测试授权
登录成功之后访问需要授权页面,未拥有权限时返回状态403
有该权限时则直接访问资源
到此使用Spring Boot整合Spring Security实现简单的权限控制完成,写这个demo只是为了熟悉流程,还有更多的知识等着我们去学习。
上一篇: 线程安全问题
推荐阅读
-
SpringBoot+Spring Security+JWT实现RESTful Api权限控制的方法
-
spring security动态配置url权限的2种实现方法
-
Spring Boot2.0整合ES5实现文章内容搜索实战
-
Spring Boot Security 结合 JWT 实现无状态的分布式API接口
-
spring security动态配置url权限的2种实现方法
-
Spring Boot2.0整合ES5实现文章内容搜索实战
-
Spring Boot整合ElasticSearch实现多版本兼容的方法详解
-
Spring MVC整合Shiro权限控制的方法
-
spring boot整合CAS Client实现单点登陆验证的示例
-
spring boot整合quartz实现多个定时任务的方法