2 Spring Security 认证研究
2.1 Spring Security介绍
认证功能几乎是每个项目都要具备的功能,并且它与业务无关,市面上有很多认证框架,如:Apache Shiro、CAS、Spring Security等。由于本项目基于Spring Cloud技术构建,Spring Security是spring家族的一份子且和Spring Cloud集成的很好,所以本项目选用Spring Security作为认证服务的技术框架。
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是一个专注于为 Java 应用程序提供身份验证和授权的框架。
项目主页:https://spring.io/projects/spring-security
Spring cloud Security: https://spring.io/projects/spring-cloud-security
2.2 认证授权入门
2.2.1 创建认证服务工程
下边我们使用Spring Security框架快速构建认证授权功能体系。
1、部署认证服务工程
从课程资料中拷贝xuecheng-plus-auth工程到自己的工程目录下。
此工程是一个普通的spring boot工程,可以连接数据库。
此工程不具备认证授权的功能。
2、创建数据库
创建users数据库
导入课程资料中的xcplus_users.sql脚本。
在nacos中新增auth-service-dev.yaml:
YAMLserver: servlet: context-path: /auth port: 63070 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.101.65:3306/xc1010_users?serverTimezone=UTC&userUnicode=true&useSSL=false& username: root password: mysql |
---|
初始工程自带了一个Controller类,如下:
Javapackage com.xuecheng.auth.controller; /** * @author Mr.M * @version 1.0 * @description 测试controller * @date 2022/9/27 17:25 */ @Slf4j @RestController public class LoginController { @Autowired XcUserMapper userMapper; @RequestMapping(“/login-success”) public String loginSuccess(){ return “登录成功”; } @RequestMapping(“/user/{id}”) public XcUser getuser(@PathVariable(“id”) String id){ XcUser xcUser = userMapper.selectById(id); return xcUser; } @RequestMapping(“/r/r1”) public String r1(){ return “访问r1资源”; } @RequestMapping(“/r/r2”) public String r2(){ return “访问r2资源”; } } |
---|
启动工程,尝试访问http://localhost:63070/auth/r/r1 :
访问用户信息:http://localhost:63070/auth/user/52
以上测试一切正常说明此工程部署成功。
2.2.2 认证测试
下边向springboot工程集成Spring security,
向pom.xml加入Spring Security所需要的依赖
Java org.springframework.cloud spring-cloud-starter-security org.springframework.cloud spring-cloud-starter-oauth2 |
---|
重启工程,访问http://localhost:63070/auth/r/r1
自动进入/login登录页面,/login是spring security提供的,此页面有几个css样式加载会稍微慢点,如下图:
账号和密码是多少呢?下一步需要进行安全配置。
拷贝课程资料下的WebSecurityConfig.java到config下需要三部分内容:
1、用户信息
在内存配置两个用户:zhangsan、lisi
zhangsan用户拥有的权限为p1
lisi用户拥有的权限为p2
2、密码方式
暂时采用明文方式
3、安全拦截机制
/r/**开头的请求需要认证
登录成功到成功页面
代码如下:
Java//配置用户信息服务 @Bean public UserDetailsService userDetailsService() { //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中 InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build()); manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build()); return manager; } @Bean public PasswordEncoder passwordEncoder() { //密码为明文方式 return NoOpPasswordEncoder.getInstance(); } //配置安全拦截机制 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(“/r/**”).authenticated()//访问/r开始的请求需要认证通过 .anyRequest().permitAll()//其它请求全部放行 .and() .formLogin().successForwardUrl(“/login-success”);//登录成功跳转到/login-success http.logout().logoutUrl(“/logout”);//退出地址 } |
---|
重启工程
1、访问http://localhost:63070/auth/user/52 可以正常访问
2、访问http://localhost:63070/auth/r/r1 显示登录页面
账号zhangsan,密码为123,如果输入的密码不正确会认证失败,输入正确显示登录成功。
为什么http://localhost:63070/auth/user/52 可以正常访问,访问http://localhost:63070/auth/r/r1 显示登录页面?
http.logout().logoutUrl(“/logout”);配置了退出页面,认证成功后访问/logout可退出登录。
2.2.3 授权测试
用户认证通过去访问系统资源时spring security进行授权控制,判断用户是否有该资源的访问权限,如果有则继续访问,如果没有则拒绝访问。
下边测试授权功能:
1、配置用户拥有哪些权限。
在WebSecurityConfig类配置zhangsan拥有p1权限,lisi拥有p2权限。
Java @Bean public UserDetailsService userDetailsService() { //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中 InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername(“zhangsan”).password(“123”).authorities(“p1”).build()); manager.createUser(User.withUsername(“lisi”).password(“456”).authorities(“p2”).build()); return manager; } |
---|
2、指定资源与权限的关系。
什么是系统的资源?
比如:查询一个用户的信息,用户信息就是系统的资源,要访问资源需要通过URL,所以我们在controller中定义的每个http的接口就是访问资源的接口。
下边在controller中配置/r/r1需要p1权限,/r/r2需要p2权限。
hasAuthority(‘p1’)表示拥有p1权限方可访问。
代码如下:
Java@RestController public class LoginController { … @RequestMapping(“/r/r1”) @PreAuthorize(“hasAuthority(‘p1’)”)//拥有p1权限方可访问 public String r1(){ return “访问r1资源”; } @RequestMapping(“/r/r2”) @PreAuthorize(“hasAuthority(‘p2’)”)//拥有p2权限方可访问 public String r2(){ return “访问r2资源”; } … |
---|
现在重启工程。
当访问以/r/开头的url时会判断用户是否认证,如果没有认证则跳转到登录页面,如果已经认证则判断用户是否具有该URL的访问权限,如果具有该URL的访问权限则继续,否则拒绝访问。
例如:
访问/r/r1,使用zhangsan登录可以正常访问,因为在/r/r1的方法上指定了权限p1,zhangsan用户拥有权限p1,所以可以正常访问。
访问/r/r1,使用lisi登录则拒绝访问,由于lisi用户不具有权限p1需要拒绝访问
注意:如果访问上不加@PreAuthorize,此方法没有授权控制。
整理授权的过程见下图所示:
2.2.4 工作原理
通过测试认证和授权两个功能,我们了解了Spring Security的基本使用方法,下边了解它的工作流程。
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对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)进行处理。
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
Spring Security的执行流程如下:
用户提交用户名、密码被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。