Spring Security 安全框架应用原理解析
Spring Security 简介
背景分析
企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,有些只能登录以后才能访问,还有一些你登录成功以后,权限不够也不能访问.总之这些规则都是保护系统资源不被破坏的一种手段.几乎每个系统中都需要这样的措施对数据(资源)进行保护.我们通常会通过软件技术对这样业务进行具体的设计和实现.早期没有统一的标准,每个系统都有自己独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共享做了具体的落地实现,例如Spring Security,Apache shiro诞生了.
认证授权分析
用户在进行资源访问时,要求系统要对用户进行权限控制,其具体流程如图所示:
Spring Security 概述
Spring Security 是一个企业级安全框架,由spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在springboot技术推出以后,配置方面做了很大的简化.市场上现在的分布式架构下的安全控制正在逐步的转向Spring Security.
Spring Security 基本架构
Spring Security 在企业中实现认证和授权业务时,底层构建了大量的过滤器.
其中:
绿色部分为认证过滤器,需要我们自己配置,也可以配置过个认证过滤器.也可以使用Spring Security提供的默认认证过滤器.黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.
Spring Security 快速入门
创建工程
添加项目依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.3.2.RELEASE</version> </parent> <groupId>com.cy</groupId> <artifactId>02-jt-spring-security</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
创建配置文件
在resources目录下创建application.yml文件,并指定服务端口
server: port: 8080
创建项目启动类
package com.cy.jt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringSecurityApplication { public static void main(String[] args) { SpringApplication.run( SpringSecurityApplication.class, args); } }
运行启动类访问测试
第一步:检查控制输出,是否自动生成了一个密码,例如:
Using generated security password: 360123aa-df93-4cd9-bab4-5212af421d2c
第二步:打开浏览器输入http://localhost:8080,然后呈现登录页面,例如:
在登录窗口中输入用户名user(系统默认),密码(服务启动时,控制台默认输出的
密码),然后点击Sign in进行登录,登录成功默认会出现,如下界面:
定义登录成功页面
在项目的resources目录下创建static目录,并在此目录创建一个index.html文件,例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>Login Ok</h1> </body> </html>
启动服务,再次进行登录访问测试,登录成功以后系统默认会跳转到index.html页面,例如
配置登录密码
第一步:编写一个方法(可以在启动类中调用执行),对一个名文进行加密,例如:
static void encodePwd(){ BCryptPasswordEncoder encoder=new BCryptPasswordEncoder(); String password="123456";//明文 String newPwd=encoder.encode("123456"); System.out.println(newPwd);//$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr. }
第二步:将用户和密码在在springboot工程的application.yml文件中进行配置,例如:
spring: security: user: name: jack #password: 123456 #这种写法,密码太简单了 password: '{bcrypt}$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr.'
其中,{bcrypt}指定了密码加密时使用的算法
第三步:启动服务,重新进行登录测试.
SpringSecurity 认证逻辑实现
自定义登陆逻辑
SpringSecurity支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,那就是系统上线后,用户信息的变更比较麻烦。因此SpringSecurity还支持通过实现UserDetailsService接口的方式来提供用户认证授权信息,其应用过程如下:
第一步:定义security配置类,例如:
/** * 由@Configuration注解描述的类为spring中的配置类,配置类会在spring * 工程启动时优先加载,在配置类中通常会对第三方资源进行初始配置. */ @Configuration public class SecurityConfig { /** * 定义SpringSecurity密码加密对象 * @Bean 注解通常会在@Configuration注解描述的类中描述方法, * 用于告诉spring框架这个方法的返回值会交给spring管理,并spring * 管理的这个对象起个默认的名字,这个名字与方法名相同,当然也可以通过 * @Bean注解起名字 */ @Bean //对象名默认为方法名 //@Bean("bcryptPasswordEncoder")//bean对象名字为bcryptPasswordEncoder public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
第二步:定义UserDetailService接口实现类,自定义登陆逻辑,代码如下:
UserDetailService为SpringSecurity官方提供的登录逻辑处理对象,我们自己可以实现此接口,然后在对应的方法中进行登录逻辑的编写即可.
package com.cy.jt.security.service; @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private BCryptPasswordEncoder passwordEncoder; /** * 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法. * @param username 这个参数为页面输出的用户名 * @return 一般是从数据库基于用户名查询到的用户信息 * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.基于用户名从数据库查询用户信息 //User user=userMapper.selectUserByUsername(username); if(!"jack".equals(username))//假设这是从数据库查询的信息 throw new UsernameNotFoundException("user not exists"); //2.将用户信息封装到UserDetails对象中并返回 //假设这个密码是从数据库查询出来的 String encodedPwd=passwordEncoder.encode("123456"); //假设这个权限信息也是从数据库查询到的 //假如分配权限的方式是角色,编写字符串时用"ROLE_"做前缀 List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList( "ROLE_admin,ROLE_normal,sys:res:retrieve,sys:res:create"); //这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息 //后续我们也可以基于需要自己构建UserDetails接口的实现 User user=new User(username,encodedPwd,grantedAuthorities); return user; } }
说明,这里的User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.
第三步:启动服务进行登陆,访问测试。
自定义登陆页面
第一步:定义登陆页面(直接在static目录下创建即可),关键代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Please sign in</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> </head> <body> <div class="container"> <form class="form-signin" method="post" action="/login"> <h2 class="form-signin-heading">Please sign in</h2> <p> <label for="username" class="sr-only">Username</label> <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus> </p> <p> <label for="password" class="sr-only">Password</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password" required> </p> <input name="_csrf" type="hidden" value="cc1471a5-3246-43ff-bef7-31d714273899" /> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> </form> </div> </body> </html>
注意:请求的url暂时为”/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePasswordAuthenticationFilter中进行了定义。
第二步:修改安全配置类,让其实现接口,并重写相关config方法,进行登陆设计,代码如下:
@Configuration public class SecutiryConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); //关闭跨域攻击,不关闭容易出错 http.csrf().disable(); //自定义登陆表单 http.formLogin() //设置登陆页面 .loginPage("/login.html") //设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象 .loginProcessingUrl("/login") //设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步) .usernameParameter("username") //请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步) .passwordParameter("password") //设置登陆成功跳转页面(默认为/index.html) .defaultSuccessUrl("/index.html") //登陆失败访问的页面(默认为/login.html?error) .failureUrl("/login.html?error"); //认证设计 http.authorizeRequests() //设置要放行的咨询 .antMatchers("/login.html").permitAll() //设置需要认证的请求(除了上面的要放行,其它都要进行认证) .anyRequest().authenticated(); } }
登陆成功和失败处理器
现在的很多系统都采用的是前后端分离设计,我们登陆成功以后可能会跳转到前端系统的某个地址,或者返回一个json数据,我们可以自己定义登录成功的处理操作,例如:
定义登陆成功处理器:
方案1:可以直接执行重定向的处理器,例如
package com.cy.jt.auth.config.authentication; public class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler { //定义要跳转的url private String redirectUrl; public RedirectAuthenticationSuccessHandler(String redirectUrl){ this.redirectUrl=redirectUrl; } @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.sendRedirect(redirectUrl); } }
方案2:可以直接返回JSON数据的处理器,例如:
package com.cy.jt.security.config.handler; /**处理登录失败 * 0)Default-默认 * 1)Authentication-认证 * 2)Failure-失败 * 3)Handler-处理器 * */ public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { //1.设置响应数据的编码 httpServletResponse.setCharacterEncoding("utf-8"); //2.告诉客户端响应数据的类型,以及客户端以怎样的编码进行显示 httpServletResponse.setContentType("application/json;charset=utf-8"); //3.获取一个输出流对象 PrintWriter out=httpServletResponse.getWriter(); //4.向客户端输出一个json格式字符串 //4.1构建一个map对象 Map<String,Object> map=new HashMap<>(); map.put("state","500"); map.put("msg","username or password error"); //4.2基于jackson中的ObjectMapper对象将一个对象转换为json格式字符串 String jsonStr= new ObjectMapper().writeValueAsString(map); out.println(jsonStr); out.flush(); } }
定义登陆失败处理器:
方案1:登陆失败重定向到页面,例如
package com.cy.jt.auth.config.authentication; public class RedirectAuthenticationFailureSuccessHandler implements AuthenticationFailureHandler { private String redirectUrl; public RedirectAuthenticationFailureSuccessHandler(String redirectUrl){ this.redirectUrl=redirectUrl; } @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.sendRedirect(redirectUrl); } }
方案2:定义登陆失败处理器,例如:
package com.cy.jt.security.config.handler; /**处理登录失败 * 0)Default-默认 * 1)Authentication-认证 * 2)Failure-失败 * 3)Handler-处理器 * */ public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { //1.设置响应数据的编码 httpServletResponse.setCharacterEncoding("utf-8"); //2.告诉客户端响应数据的类型,以及客户端以怎样的编码进行显示 httpServletResponse.setContentType("application/json;charset=utf-8"); //3.获取一个输出流对象 PrintWriter out=httpServletResponse.getWriter(); //4.向客户端输出一个json格式字符串 //4.1构建一个map对象 Map<String,Object> map=new HashMap<>(); map.put("state","500"); map.put("msg","username or password error"); //4.2基于jackson中的ObjectMapper对象将一个对象转换为json格式字符串 String jsonStr= new ObjectMapper().writeValueAsString(map); out.println(jsonStr); out.flush(); } }
修改配置类,设置登陆成功与失败处理器。
@Configuration public class SecutiryConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //super.configure(http); //关闭跨域攻击,不关闭容易出错 http.csrf().disable(); //自定义登陆表单 http.formLogin() //设置登陆页面 .loginPage("/login.html") //设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象 .loginProcessingUrl("/login") //设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步) .usernameParameter("username") //请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步) .passwordParameter("password") //设置登陆成功跳转页面(默认为/index.html) .successHandler(new RedirectAuthenticationSuccessHandler("你的url")) //登陆失败访问的页面(默认为/login.html?error) .failureHandler(new RedirectAuthenticationFailureHandler("你的url")) //认证设计 http.authorizeRequests() //设置要放行的咨询 .antMatchers("/login.html").permitAll() //设置需要认证的请求(除了上面的要放行,其它都要进行认证) .anyRequest().authenticated(); } }
第四步:启动服务进行访问测试(分别用正确和错误的账号进行测试)。
放行静态资源
在SecurityManager配置类中的configure(HttpSecurity http)方法中我们可以通过对anMatchers方法定义要放行静态资源,例如:
.authorizeRequests() //设置请求的授权 .antMatchers( //配置下列路径的授权 "/index.html", "/js/*", "/css/*", "/img/**", "/bower_components/**", "/login.html" ).permitAll() //设置上述所有路径不需要登录就能访问(放行)
其中:
- “*”用于匹配0个或多个字符
- “**”用于匹配0个或多个目录及字符
登出设计及实现
在SecurityManager配置类中的configure(HttpSecurity http)方法中,添加登出配置,例如
http.logout() //开始设置登出信息 .logoutUrl("/logout") //登出路径 .logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面
SpringSecurity授权逻辑实现
修改授权配置类
在权限配置类上添加启用全局方法访问控制注解,例如:
package com.cy.auth.config; //这个配置类是配置Spring-Security的, //prePostEnabled= true表示启动权限管理功能 @EnableGlobalMethodSecurity(prePostEnabled = true) @Configuration public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter { …… }
定义资源Controller
定义一个ResourceController类,作为资源访问对象,例如
package com.cy.jt.auth.controller; @RestController public class ResourceController { @PreAuthorize("hasAuthority('sys:res:create')") @RequestMapping("/doCreate") public String doCreate(){ return "add resource"; } @PreAuthorize("hasAuthority('sys:res:update')") @RequestMapping("doUpdate") public String doUpdate(){ return "update resource"; } @PreAuthorize("hasAuthority('sys:res:delete')") @RequestMapping("/doDelete") public String doDelete(){ return "delete resource"; } @PreAuthorize("hasAuthority('sys:res:retrieve')") @RequestMapping("/doRetrieve") public String doRetrieve(){ return "retrieve resource"; } }
其中,@PreAuthorize注解描述方法时,用于告诉系统访问此方法时需要进行权限检测。需要具备指定权限才可以访问。例如:
- @PreAuthorize(“hasAuthority('sys:res:delete”) 需要具备sys:res:delete权限
- @PreAuthorize(“hasRole(‘admin')”) 需要具备admin角色 启动服务访问测试
使用不同用户进行登陆,然后执行资源访问,假如没有权限,则会看到响应状态吗403,如图所示:
Spring认证和授权异常处理
异常类型
对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:
1)AuthenticationException (用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401)
2)AccessDeniedException (用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403)
异常处理规范
SpringSecurity框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务需求时,此时我们就要自己定义异常处理逻辑,编写逻辑时需要遵循如下规范:
1)AuthenticationEntryPoint:统一处理 AuthenticationException 异常
2)AccessDeniedHandler:统一处理 AccessDeniedException 异常.
自定义异常处理对象
处理没有认证的访问异常
package com.cy.jt.config; public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { //设置响应数据的编码 response.setCharacterEncoding("utf-8"); //告诉浏览器要响应的内容类型,以及编码 response.setContentType("application/json;charset=utf-8"); Map<String,Object> map=new HashMap<>(); map.put("state",401); map.put("message","请先登录"); PrintWriter out=response.getWriter(); out.println(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } }
处理没有权限时抛出的异常
package com.cy.jt.config; public class DefaultAccessDeniedExceptionHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { //设置响应数据的编码 response.setCharacterEncoding("utf-8"); //告诉浏览器要响应的内容类型,以及编码 response.setContentType("application/json;charset=utf-8"); Map<String,Object> map=new HashMap<>(); map.put("state",403); map.put("message","没有此资源的访问权限"); PrintWriter out=response.getWriter(); out.println(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } }
配置异常处理对象
在配置类SecurityConfig中添加自定义异常处理对象,代码如下
http.exceptionHandling() .authenticationEntryPoint(new DefaultAuthenticationEntryPoint()) .accessDeniedHandler(new DefaultAccessDeniedExceptionHandler());
配置完成后,重启服务进行访问测试分析.
系统会话状态分析与实践 何为会话状态
客户端与服务端通讯过程中产生的状态信息(类似会议记录),称之为会话状态.
会话状态如何存储
客户端浏览器与服务端通讯时使用的是http协议,这个协议本身是无状态协议,也就是说通过此协议,无法存储会话状态,此时在服务端与客户端就采用了一种Cookie与Session方式记录会话状态.
有状态的会话技术分析
- Cookie 技术
Cookie是由服务端创建但在客户端存储会话状态的一个对象,此对象分为两种类型,一种为会话Cookie,一种为持久Cookie,浏览器在访问具体的某个域名时会携带这个域的有效Cookie到服务端.
- 会话Cookie: 浏览器关闭Cookie生命周期结束(一般默认都是会话Cookie)
- 持久Cookie: 持久Cookie是在Cookie对象创建时指定了生命周期,例如一周时间,即便浏览器关闭,持久Cookie依旧有效.
- Session技术
Session技术由服务端创建,并在服务端存储会话状态的一个对象,当Session对象创建时,还会创建一个会话Cookie对象,并且通过这个会话Cookie将SessionId写到客户端,客户端下次访问服务端会携带这个会话Cookie,并且通过JsessionId找到Session对象,进而获取Session对象中存储的数据.Cookie默认的生命周期为30分钟.
在SpringSecurity中获取用户的认证信息,就可以通过如下方式进行实现:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
无状态的会话技术分析
有状态的会话实现,在分布式架构中可能会存在很多问题,例如浏览器默认不支持携带其它域的Cookie信息进行资源访问,同时服务端的Session默认不能共享,当然我们有一种方式可以将session持久化到到一些数据库,例如Redis,下次请求到其它服务器(例如tomcat)时,可以直接从redis中获取登录信息,但是假如并发比较大,数据库的访问压力就会剧增,压力太大有可能会导致系统宕机.所以现在还有一种方案就是将用户的登录状态信息都存储在客户端,服务端不记录任何状态,服务端只负责对客户端传递过来的状态信息进行解析,基于此方式进行用户登录状态的判断,这样的会话过程称之为无状态会话.
总结(Summary)
重难点分析
- SpringSecurity 产生背景?
- SpringSecurity 快速入门?(依赖,配置,登录认证,密码的加密-启动生成,配置文件)
- SpringSecurity 认证逻辑分析及实践?(认证方式-用户名和密码,登录页
- 面,SecurityConfig.UserServiceDetail,成功,失败,放行)
- SpringSecurity 授权逻辑分析及实现?(为什么,授权步骤,用到的注解)
FAQ 分析
- 如何理解认证?(判定用户身份的合法性)
- 如何校验用户身份的合法性?(用户密码,指纹,刷脸,刷身份证,…)
- 如何进行身份认证?(自己写认证逻辑,借助框架去写认证逻辑-尊重框架规则)
- 市场上的认证和授权框架有哪些?(SpringSecurity,Shiro)
- 为什么会选择SpringSecurity?(功能强大,SpringBoot诞生后在配置方面做了大量的简化)
- SpringSecurity中的加密方式你用的什么?(Bcrypt,底层基于随机盐方式对密码进行hash不可逆加密,更加安全,缺陷是慢)
- SpringSecurity中你用过哪些API?(BcryptPasswordEncoder,UserDetailService,UserDetail,User,
- AuthenticationSuccessHandler,AuthenticationFailureHandler,…)
- 为什么要进行权限控制?(防止非法用户破坏数据)
- SpringSecurity进行权限控制的步骤(@EnableGlobalMethodSecurity,@PreAuthorize)
- SpringSecurity在进行认证和授权时可能出现的异常?
- SpringSecurity在未认证和未授权的前提下访问授权资源时,出现的异常如何处理?
- 作业:用户登录成功以后,用户信息默认存在哪里了? (Session)
- 作业:用户登录成功以后,如何获取我们登录的用户信息?(这个用户的用户名,这个用户的权限)
Bug 分析
- 依赖下载不完整
- 响应json数据时出现文件下载
到此这篇关于Spring Security 安全框架应用的文章就介绍到这了,更多相关Spring Security框架内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!