本文最后更新于:2021年4月14日 晚上
Spring Security
基于 Spring Security 5.1.4.RELEASE
1. 基本概念 1.1 认证
判断一个用户的身份是否合法 的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
1.2 会话 认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前 用户的登录状态 所提供的机制。
认证成功后,服务器生成用户相关数据保存在session(当前会话)中,发给客户端 session_id
,客户机存在 cookie
中,下次客户端请求会带上 session_id ,服务器可以验证 session 数据,检验合法性,当用户退出系统或 session 过期,session_id 就无效。
用户认证成功后,服务端生成一个token
发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
区别:
1.3 授权 用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
区别于认证
数据模型 授权可简单理解为:主体 对 资源 进行 怎样的操作 「权限/许可」,权限是需要与资源进行关联的,否则权限没有意义。
三者的关系图:
不同的权限可以管理不同的资源,用户需要有对应权限才能访问资源。
引入一个 「角色」概念,用户归类于角色 ,角色拥有相应权限 ,最后根据权限访问资源
用户【用户id,账号,密码,…】
角色【角色id,角色名称,…】
权限【权限id,权限标识,权限名称,资源id,…】
资源【资源id,资源名称,访问地址,…】
用户 - 角色 【用户id,角色id,…】
角色 - 权限 【角色id,权限id,….】
企业开发将 资源和权限合为一张权限表:
资源【资源id、资源名称、访问地址、…】
权限【权限id、权限标识、权限名称、资源id、…】
合并 为:权限【权限id、权限标识、权限名称、资源名称、资源访问地址、…】
1.4 RBAC 1.4.1 基于角色的访问控制 Role-Based Access Control
按照角色进行授权。
if (主体.hasRole("总经理角色id" )) { 查询工资 }
如果上图所需角色添加为老板和总经理,此时就要改代码
if (主体.hasRole("总经理角色id" ) || 主体.hasRole("老板角色id" )) { 查询工资 }
1.4.2 基于资源的访问控制 Resource-Based Access Control
按照资源(权限)进行授权。
if (主体.hasPermission("查询工资权限标识" )) { 查询工资 }
2. 基于Session的认证方式「手动实现security」 2.1 环境搭建
基于 SpringMVC 5.1.5.RELEASE, Servlet3.0实现
2.1.1 创建Maven工程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 <?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 > <groupId > com.xxx.security</groupId > <artifactId > security-springmvc</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > war</packaging > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.1.5.RELEASE</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.0.1</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.8</version > </dependency > </dependencies > <build > <finalName > security-springmvc</finalName > <pluginManagement > <plugins > <plugin > <groupId > org.apache.tomcat.maven</groupId > <artifactId > tomcat7-maven-plugin</artifactId > <version > 2.2</version > <configuration > <port > 8080</port > <path > /</path > <uriEncoding > utf-8</uriEncoding > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 1.8</source > <target > 1.8</target > </configuration > </plugin > <plugin > <artifactId > maven-resources-plugin</artifactId > <configuration > <encoding > utf-8</encoding > <useDefaultDelimiters > true</useDefaultDelimiters > <resources > <resource > <directory > src/main/resources</directory > <filtering > true</filtering > <includes > <include > **/*</include > </includes > </resource > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > </resource > </resources > </configuration > </plugin > </plugins > </pluginManagement > </build > </project >
Web项目,packaging打包方式为 war
使用tomcat7-maven-plugin
插件来运行工程
2.1.2 Spring容器配置
config包下定义ApplicationConfig
,它对应web.xml中ContextLoaderListener
的配置「Servlet3.0不用xml配置」
@Configuration @ComponentScan(basePackages = "com.xxx.security.springmvc" ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) public class ApplicationConfig { }
2.1.3 servletContext配置
config包下定义 WebConfig
,对应DispatcherServlet
配置。
@Configuration @EnableWebMvc @ComponentScan(basePackages = "com.xxx.security.springmvc" ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) public class WebConfig implements WebMvcConfigurer { @Bean public InternalResourceViewResolver viewResolver () { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/view/" ); viewResolver.setSuffix(".jsp" ); return viewResolver; } }
2.1.4 加载 Spring容器
在init包下定义Spring容器初始化类SpringApplicationInitializer
,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[]{ApplicationConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } @Override protected String[] getServletMappings() { return new String[]{"/" }; } }
SpringApplicationInitializer相当于web.xml,ApplicationConfig.java对应以下配置的application-context.xml,WebConfig.java对应以下配置的spring-mvc.xml ,web.xml 的内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <web‐app> <listener> <listener‐class >org .springframework .web .context .ContextLoaderListener </listener ‐class > </listener > <context ‐param > <param ‐name >contextConfigLocation </param ‐name > <param ‐value >/WEB ‐INF /application ‐context .xml </param ‐value > </context ‐param > <servlet > <servlet ‐name >springmvc </servlet ‐name > <servlet ‐class >org .springframework .web .servlet .DispatcherServlet </servlet ‐class > <init ‐param > <param ‐name >contextConfigLocation </param ‐name > <param ‐value >/WEB ‐INF /spring ‐mvc .xml </param ‐value > </init ‐param > <load ‐on ‐startup >1</load ‐on ‐startup > </servlet > <servlet ‐mapping > <servlet ‐name >springmvc </servlet ‐name > <url ‐pattern >/</url ‐pattern > </servlet ‐mapping > </web ‐app >
2.2 实现认证功能 2.2.1 认证页面
在webapp/WEB-INF/views
下定义认证页面login.jsp
,作为测试认证流程,页面为form表单,包括用户名,密码,触发登录将提交表单信息至/login
「controller」。
<%@ page contentType="text/html;charset=UTF‐8" pageEncoding="utf‐8" %> <html> <head> <title>用户登录</title> </head> <body> <form action="login" method="post" > 用户名:<input type="text" name="username" ><br> 密 码: <input type="password" name="password" ><br> <input type="submit" value="登录" > </form> </body> </html>
在WebConfig中新增如下配置,将/直接导向login.jsp页面
@Override public void addViewControllers (ViewControllerRegistry registry) { registry.addViewController("/" ).setViewName("login" ); }
2.2.2 认证接口
定义认证接口,此接口对传来的用户名密码校验,若成功则返回该用户的详细信息,否则抛出错误异常
在service包下创建AuthenticationService
接口
public interface AuthenticationService { UserDto authentication (AuthenticationRequest authenticationRequest) ; }
@Data public class AuthenticationRequest { private String username; private String password; }
model包下,创建UerDto,作为登录成功后获得的登录用户的信息
@Data @AllArgsConstructor public class UserDto { private String id; private String username; private String password; private String fullname; private String mobile; private Set<String> authorities; }
认证实现类,根据用户名查找用户信息,并校验密码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Service public class AuthenticationServiceImpl implements AuthenticationService { { Set<String> authorities1 = new HashSet<>(); authorities1.add("p1" ); Set<String> authorities2 = new HashSet<>(); authorities2.add("p2" ); userMap.put("zhangsan" , new UserDto("1010" , "zhangsan" , "123" , "张三" , "133443" , authorities1)); userMap.put("lisi" , new UserDto("1011" , "lisi" , "456" , "李四" , "144553" , authorities2)); } @Override public UserDto authentication (AuthenticationRequest authenticationRequest) { if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername()) || StringUtils.isEmpty(authenticationRequest.getPassword())) { throw new RuntimeException("账号和密码为空" ); } UserDto user = getUserDto(authenticationRequest.getUsername()); if (user == null ) { throw new RuntimeException("查询不到该用户" ); } if (!authenticationRequest.getPassword().equals(user.getPassword())) { throw new RuntimeException("账号或密码错误" ); } return user; } private UserDto getUserDto (String userName) { return userMap.get(userName); } private Map<String, UserDto> userMap = new HashMap<>(); }
登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:
@RestController public class LoginController { @Autowired AuthenticationService authenticationService; @RequestMapping(value = "/login", produces = {"text/plain;charset=UTF-8"}) public String login (AuthenticationRequest authenticationRequest, HttpSession session) { UserDto userDto = authenticationService.authentication(authenticationRequest); return userDto.getUsername() + "登录成功" ; } }
Test,访问路径为 /
至此,完成最简单的身份凭证校验,后面对于资源的请求还要单独授权。
2.2.3 实现会话功能
增加会话控制
在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key
public static final String SESSION_USER_KEY = "_user" ;
修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为 失效。
@RequestMapping(value = "/login", produces = {"text/plain;charset=UTF-8"}) public String login (AuthenticationRequest authenticationRequest, HttpSession session) { UserDto userDto = authenticationService.authentication(authenticationRequest); session.setAttribute(UserDto.SESSION_USER_KEY, userDto); return userDto.getUsername() + "登录成功" ; }@GetMapping(value = "/logout", produces = {"text/plain;charset=UTF-8"}) public String logout (HttpSession session) { session.invalidate(); return "退出成功" ; }
增加测试资源
修改LoginController,增加测试r1,它从当前会话session中获取当前登录用户,并返回提示信息给前台。
@GetMapping(value = "/r/r1", produces = {"text/plain;charset=UTF-8"}) public String r1 (HttpSession session) { String fullname = null ; Object object = session.getAttribute(UserDto.SESSION_USER_KEY); if (object == null ) { fullname = "匿名" ; } else { UserDto userDto = (UserDto) object; fullname = userDto.getFullname(); } return fullname + "访问资源r1" ; }
Test
在用户登录成功时,该用户信息已被成功放入session,并且后续请求可以正常从session中获取当 前登录用户信息,符合预期结果。
2.2.4 实现授权功能
匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
登录用户访问拦截:根据用户的权限 决定是否能访问某些资源。
增加权限数据
private Set<String> authorities;
在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。
private Map<String,UserDto> userMap = new HashMap<>(); { Set<String> authorities1 = new HashSet<>(); authorities1.add("p1" ); Set<String> authorities2 = new HashSet<>(); authorities2.add("p2" ); userMap.put("zhangsan" ,new UserDto("1010" ,"zhangsan" ,"123" ,"张 三" ,"133443" ,authorities1)); userMap.put("lisi" ,new UserDto("1011" ,"lisi" ,"456" ,"李四" ,"144553" ,authorities2)); }
增加测试资源,同/r/r1,这是资源r2
@GetMapping(value = "/r/r2", produces = {"text/plain;charset=UTF-8"}) public String r2 (HttpSession session) { String fullname = null ; Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY); if (userObj != null ) { fullname = ((UserDto) userObj).getFullname(); } else { fullname = "匿名" ; } return fullname + " 访问资源r2" ; }
实现授权拦截器
在interceptor
包下定义SimpleAuthenticationInterceptor
拦截器
实现 1、校验用户是否登录 2、校验用户是否拥有操作权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class SimpleAuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY); if (object == null ) { writeContent(response, "请登录" ); } UserDto userDto = (UserDto) object; String requestURI = request.getRequestURI(); if (userDto.getAuthorities().contains("p1" ) && requestURI.contains("/r/r1" )) { return true ; } if (userDto.getAuthorities().contains("p2" ) && requestURI.contains("/r/r2" )) { return true ; } writeContent(response, "没有权限,拒绝访问" ); return false ; } private void writeContent (HttpServletResponse response, String msg) throws IOException { response.setContentType("text/html;charset=utf-8" ); PrintWriter writer = response.getWriter(); writer.print(msg); writer.close(); } }
在 WebConfig中配置拦截器,匹配/r/**
的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor拦截器。
@Autowired private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**" ); }
Test
3. Spring Security 快速上手「基于MVC」
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入Spring Security更是十分简单,使用Spring Security减少了为企业系统安全控制编写大量重复代码的工作。
3.1 环境搭建 3.1.1 创建Maven工程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 <groupId > com.itheima.security</groupId > <artifactId > security-spring-security</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > war</packaging > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-web</artifactId > <version > 5.1.4.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-config</artifactId > <version > 5.1.4.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 5.1.5.RELEASE</version > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.0.1</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.8</version > </dependency > </dependencies > <build > <finalName > security-springmvc</finalName > <pluginManagement > <plugins > <plugin > <groupId > org.apache.tomcat.maven</groupId > <artifactId > tomcat7-maven-plugin</artifactId > <version > 2.2</version > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 1.8</source > <target > 1.8</target > </configuration > </plugin > <plugin > <artifactId > maven-resources-plugin</artifactId > <configuration > <encoding > utf-8</encoding > <useDefaultDelimiters > true</useDefaultDelimiters > <resources > <resource > <directory > src/main/resources</directory > <filtering > true</filtering > <includes > <include > **/*</include > </includes > </resource > <resource > <directory > src/main/java</directory > <includes > <include > **/*.xml</include > </includes > </resource > </resources > </configuration > </plugin > </plugins > </pluginManagement > </build >
3.1.2 Spring容器配置
@Configuration @ComponentScan(basePackages = "com.xxx.security.springmvc" ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) public class ApplicationConfig { }
3.1.3 Servlet Context配置
@Configuration @EnableWebMvc @ComponentScan(basePackages = "com.xxx.security.springmvc" ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)}) public class WebConfig implements WebMvcConfigurer { @Bean public InternalResourceViewResolver viewResolver () { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB‐INF/views/" ); viewResolver.setSuffix(".jsp" ); return viewResolver; } }
3.1.4 加载 Spring容器
在init
包下定义Spring容器初始化类SpringApplicationInitializer
,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { ApplicationConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; } @Override protected String[] getServletMappings() { return new String [] {"/" }; } }
3.2 认证 3.2.1 认证页面
Spring Security默认提供认证页面,不需要额外开发。
3.2.2 安全配置
Spring Security提供了用户名密码登录 、退出 、会话管理 等认证功能,只需要配置即可使用。
在config包下定义WebSecurityConfig
,作为Security框架的配置类,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @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/r1" ).hasAuthority("p1" ) .antMatchers("/r/r2" ).hasAuthority("p2" ) .antMatchers("/r/**" ).authenticated() .anyRequest().permitAll() .and() .formLogin() .successForwardUrl("/login-success" ); } }
需要说明的是:
@EnableWebSecurity
注解已经包含了 @Configuration
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity {...}
在 userDetailsService()
方法中,返回一个UserDetailsService 给 Spring容S器,Spring Security会使用它来 获取用户信息。暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsan、lisi两个用 户,并设置密码和权限。InMemoryUserDetailsManager
是 UserDetailsService
接口实现类。
在configure()中,通过HttpSecurity设置了安全拦截规则
1️⃣url匹配/r/**
的资源,经过认证后才能访问
2️⃣其他url完全开放
3️⃣支持form表单认证,认证成功后转向/login-success
加载 WebSecurityConfig
修改SpringApplicationInitializer
的getRootConfigClasses()
方法,添加 WebSecurityConfig.class
@Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { ApplicationConfig.class, WebSecurityConfig.class}; }
3.2.3 Spring Security初始化
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SpringSecurityApplicationInitializer () { } }
3.2.4 默认根路径请求
在WebConfig.java中添加默认请求根路径跳转到/login
,此url为Spring Security 提供:
这里要用重定向到默认的 /login 界面
@Override public void addViewControllers (ViewControllerRegistry registry) { registry.addViewController("/" ).setViewName("redirect:/login" ); }
3.2.5 认证成功页面
在安全配置中,认证成功将跳转到/login-success
@Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/r/r1" ).hasAuthority("p1" ) .antMatchers("/r/r2" ).hasAuthority("p2" ) .antMatchers("/r/**" ).authenticated() .anyRequest().permitAll() .and() .formLogin() .successForwardUrl("/login-success" ); }
Spring Security支持form表单认证,认证成功后转向/login-success
。
在LoginController
中定义/login-success
@RequestMapping(value = "/login‐success",produces = {"text/plain;charset=UTF‐8"}) public String loginSuccess () { return " 登录成功" ; }
3.2.6 Test
启动项目,访问http://localhost:8080/security-spring-security/路径地址
页面会根据WebConfig中addViewControllers配置规则,重定向跳转至/login,/login是pring Security提供的登录页面。
登录
输入http://localhost:8080/security-spring-security/logout退出,Log Out自动跳转回登录界面
3.3 授权
@GetMapping(value = "/r/r1", produces = {"text/plain;charset=UTF-8"}) public String r1 () { return " 访问资源1" ; }@GetMapping(value = "/r/r2", produces = {"text/plain;charset=UTF-8"}) public String r2 () { return " 访问资源2" ; }
之前给两个测试用户绑定了相应的权限,表示访问”/xxx”资源的url需要有”px”的权限
manager.createUser(User.withUsername("zhangsan" ).password("123" ).authorities("p1" ).build()); manager.createUser(User.withUsername("lisi" ).password("456" ).authorities("p2" ).build()); .antMatchers("/r/r1" ).hasAuthority("p1" ) .antMatchers("/r/r2" ).hasAuthority("p2" )
张三登录,访问r1资源
张三访问r2资源,403无权限
4 Spring Security 基本原理 4.1 Spring Security本质
SpringSecurity 本质是一个过滤器链 ,从启动可以获取到过滤器链:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
重点看三个过滤器:
FilterSecurityInterceptor
一个方法级 的权限过滤器, 基本位于过滤链的最底部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { public void invoke (FilterInvocation fi) throws IOException, ServletException { if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied" ) != null && this .observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { if (fi.getRequest() != null && this .observeOncePerRequest) { fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied" , Boolean.TRUE); } InterceptorStatusToken token = super .beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super .finallyInvocation(token); } super .afterInvocation(token, (Object)null ); } } }
super.beforeInvocation(fi)
查看之前的 filter 是否通过
fi.getChain().doFilter(fi.getRequest(), fi.getResponse())
真正的调用后台的服务
ExceptionTranslationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class ExceptionTranslationFilter extends GenericFilterBean { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { chain.doFilter(request, response); this .logger.debug("Chain processed normally" ); } catch (IOException var9) { throw var9; } catch (Exception var10) { Throwable[] causeChain = this .throwableAnalyzer.determineCauseChain(var10); RuntimeException ase = (AuthenticationException)this .throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null ) { ase = (AccessDeniedException)this .throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } if (ase == null ) { if (var10 instanceof ServletException) { throw (ServletException)var10; } if (var10 instanceof RuntimeException) { throw (RuntimeException)var10; } throw new RuntimeException(var10); } if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed." , var10); } this .handleSpringSecurityException(request, response, chain, (RuntimeException)ase); } } }
UsernamePasswordAuthenticationFilter
对/login 的 POST 请求做拦截,校验表单中用户名,密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this .postOnly && !request.getMethod().equals("POST" )) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this .obtainUsername(request); String password = this .obtainPassword(request); if (username == null ) { username = "" ; } if (password == null ) { password = "" ; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this .setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); } } }
4.2 过滤器加载
过滤器加载通过 DelegatingFilterProxy
来配置加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class DelegatingFilterProxy extends GenericFilterBean { public void doFilter (ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { Filter delegateToUse = this .delegate; if (delegateToUse == null ) { synchronized (this .delegateMonitor) { delegateToUse = this .delegate; if (delegateToUse == null ) { WebApplicationContext wac = this .findWebApplicationContext(); if (wac == null ) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?" ); } delegateToUse = this .initDelegate(wac); } this .delegate = delegateToUse; } } this .invokeDelegate(delegateToUse, request, response, filterChain); } }
this.delegate = this.initDelegate(wac)
初始化代理
protected Filter initDelegate (WebApplicationContext wac) throws ServletException { String targetBeanName = this .getTargetBeanName(); Assert.state(targetBeanName != null , "No target bean name set" ); Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class); if (this .isTargetFilterLifecycle()) { delegate.init(this .getFilterConfig()); } return delegate; }
public class FilterChainProxy extends GenericFilterBean { private void doFilterInternal (ServletRequest request, ServletResponse response, FilterChain chain) { List<Filter> filters = this .getFilters((HttpServletRequest)fwRequest); } }
private List<Filter> getFilters(HttpServletRequest request)
获取所有过滤器链
4.3 两个重要接口
UserDetailsService
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口。
查询数据库用户名和密码过程写在这个接口中
public interface UserDetailsService { UserDetails loadUserByUsername (String var1) throws UsernameNotFoundException ; }
会返回 UserDetails
类,默认为 用户 “主体”
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword () ; String getUsername () ; boolean isAccountNonExpired () ; boolean isAccountNonLocked () ; boolean isCredentialsNonExpired () ; boolean isEnabled () ; }
实现类为 User
,后面只用实现User这个实体类
public class User implements UserDetails , CredentialsContainer { public User (String username, String password, Collection<? extends GrantedAuthority> authorities) { this (username, password, true , true , true , true , authorities); } }
PasswordEncoder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface PasswordEncoder { String encode (CharSequence var1) ; boolean matches (CharSequence var1, String var2) ; default boolean upgradeEncoding (String encodedPassword) { return false ; } }
BCryptPasswordEncoder
是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器,对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。
@Test public void test01 () { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String testStr = bCryptPasswordEncoder.encode("test" ); System.out.println(" 加密之后数据:\t" + testStr); boolean result = bCryptPasswordEncoder.matches("test" , testStr); System.out.println(" 比较结果:\t" + result); }
5 SpringSecurity Web 权限方案
spring-boot-starter-parent 2.2.1.RELEASE; spring-security-web 5.2.1.RELEASE
5.1 设置登录系统的账号、密码 方式一:application.properties spring.security.user.name =hypocrite30 spring.security.user.password =hypocrite30
方式二:编写类实现接口
config
包下创建 Security 的配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String password = passwordEncoder.encode("123" ); auth.inMemoryAuthentication().withUser("hypocrite30" ).password(password).roles("admin" ); } @Bean PasswordEncoder password () { return new BCryptPasswordEncoder(); } }
方式三:自定义实现类
创建配置类,设置使用哪个userDetailsService
实现类
@Configuration public class SecurityConfigTest extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(password()); } @Bean PasswordEncoder password () { return new BCryptPasswordEncoder(); } }
userDetailsService
选择使用哪个 Service进行认证
编写实现类,返回User对象,User对象有用户名密码和权限
@Service("userDetailsService" ) public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role" ); return new User ("hypocrite30" , new BCryptPasswordEncoder ().encode("password" ), auths); } }
一般都是用第三种 方法,自定义实现类。但是涉及到管理员模式,可以使用第一和第二种方式设置admin。
5.2 整合数据库完成用户认证
<dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.0.5</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0 ;DROP TABLE IF EXISTS `users`;CREATE TABLE `users` ( `id` int (0 ) NOT NULL , `username` varchar (10 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , `password` varchar (10 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ;INSERT INTO `users` VALUES (1 , 'lucy' , '123' );INSERT INTO `users` VALUES (2 , 'mary' , '456' );SET FOREIGN_KEY_CHECKS = 1 ;
@Data @AllArgsConstructor @NoArgsConstructor public class Users { private Integer id; private String username; private String password; }
整合Mybatis-plus,创建mapper,放在mapper包下
@Repository public interface UsersMapper extends BaseMapper <Users > { }
有Mapper,就需要在启动类上添加@MapperScan扫描
@MapperScan("com.xxx.security.mapper")
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true spring.datasource.username =root spring.datasource.password =root
在自定义实现类中调用mapper查询数据库用户名认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service("userDetailsService") public class MyUserDetailsService implements UserDetailsService { @Autowired private UsersMapper usersMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper(); wrapper.eq("username" , username); Users users = usersMapper.selectOne(wrapper); if (users == null ) { throw new UsernameNotFoundException("用户名不存在!" ); } List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role" ); return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths); } }
5.3 自定义登录界面 & 控制是否认证
在Security配置类进行配置,注意重写的是 形参HttpSecurity http
的 configure()
@Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/user/login" ) .successForwardUrl("/success" ) .failureForwardUrl("/fail" ); http.authorizeRequests() .antMatchers("/test/hello" , "/login" ) .permitAll() .anyRequest() .authenticated(); http.csrf().disable(); }
@PostMapping("/success") public String success () { return "success" ; }@PostMapping("/fail") public String fail () { return "fail" ; }
<form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> // username,password名称不能错 密码:<input type ="password" name ="password" /> <br /> <input type ="submit" value =" 提交" /> </form >
5.4 基于角色或权限进行访问控制 「hasAuthority」
.antMatchers("/test/index" ).hasAuthority("admins" )
List<GrantedAuthority> auths = AuthorityUtils . commaSeparatedStringToAuthorityList("admins" ) ;
「hasAnyAuthority」
.antMatchers("/test/index" ).hasAnyAuthority("admins, manager" )
「hasRole」
.antMatchers("/test/index" ).hasRole("sale" )
注:hasRole() 参数不能 加 “ROLE_
“,会抛出异常
private static String hasRole (String role) { Assert.notNull(role, "role cannot be null" ); if (role.startsWith("ROLE_" )) { throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'" ); } else { return "hasRole('ROLE_" + role + "')" ; } }
.antMatchers("/test/index" ).access("hasRole('sale')" )
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale" );
「hasAnyRole」
5.5 自定义403界面 http.exceptionHandling().accessDeniedPage("/unauth.html" );
http.exceptionHandling().accessDeniedPage("/unauth" );@GetMapping("/unauth") public String accessDenyPage () { return "unauth" ; }
5.6 注解 「@Secured」
@EnableGlobalMethodSecurity(securedEnabled=true)
可以加在启动类 或配置类 上
@SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled=true) public class DemosecurityApplication { public static void main (String[] args) { SpringApplication.run(DemosecurityApplication.class, args); } }
@GetMapping("update") @Secured({"ROLE_sale","ROLE_manager"}) public String update () { return "hello update" ; }
「@PreAuthorize」
@RequestMapping("/preAuthorize") @PreAuthorize("hasAnyAuthority('admins')") public String preAuthorize () { return "preAuthorize" ; }
「@PostAuthorize」
注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
@RequestMapping("/testPostAuthorize") @PostAuthorize("hasAnyAuthority('admins')") public String preAuthorize () { System.out.println("test--PostAuthorize" ); return "PostAuthorize" ; }
无权限登录完后虽然会虽然会进入403,但是方法里的sout依然会执行
「@PreFilter」
不常用,进入控制器之前对数据进行过滤,一般就是对特定的数据拦截
@RequestMapping("getTestPreFilter") @PreAuthorize("hasRole('admins')") @PreFilter("filterObject.id%2 == 0") public List<Users> getTestPreFilter (@RequestBody List<Users> list) { list.forEach(t -> { System.out.println(t.getId() + "\t" + t.getUsername()); }); return list; }
「@PostFilter」
不常用,权限验证之后 对数据进行过滤,留下用户名是 admin1 的数据
@GetMapping("getAll") @PostAuthorize("hasAnyAuthority('admin')") @PostFilter("filterObject.username == 'admin1'") public List<Users> getAllUser () { ArrayList<Users> list = new ArrayList<>(); list.add(new Users(11 , "admin1" , "6666" )); list.add(new Users(21 , "admin2" , "888" )); System.out.println(list); return list; }
5.7 注销
http.logout().logoutUrl("/logout" ). logoutSuccessUrl("/test/hello" ).permitAll();
测试可以先成功登录,再访问资源,退出后再访问相同资源需要登录。
5.8 RememberMe功能
CREATE TABLE `persistent_logins` ( `username` varchar (64 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `series` varchar (64 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `token` varchar (64 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `last_used` timestamp (0 ) NOT NULL , PRIMARY KEY (`series`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ;
@Autowired private DataSource dataSource;@Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
.and() .rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60 ) .userDetailsService(userDetailsService);
前端的自动登录的checkbox的name必须是 remember-me
<input type ="checkbox" name ="remember-me" /> 自动登录
「RememberMe原理」
protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this .logger.isDebugEnabled()) { this .logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); this .rememberMeServices.loginSuccess(request, response, authResult); if (this .eventPublisher != null ) { this .eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this .getClass())); } this .successHandler.onAuthenticationSuccess(request, response, authResult); }
this.rememberMeServices.loginSuccess(request, response, authResult);
继续跟进
public interface RememberMeServices { Authentication autoLogin (HttpServletRequest var1, HttpServletResponse var2) ; void loginFail (HttpServletRequest var1, HttpServletResponse var2) ; void loginSuccess (HttpServletRequest var1, HttpServletResponse var2, Authentication var3) ; }
在其实现类 AbstractRememberMeServices
中
public final void loginSuccess (HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!this .rememberMeRequested(request, this .parameter)) { this .logger.debug("Remember-me login not requested." ); } else { this .onLoginSuccess(request, response, successfulAuthentication); } }protected abstract void onLoginSuccess (HttpServletRequest var1, HttpServletResponse var2, Authentication var3) ;
onLoginSuccess
由实现类 PersistentTokenBasedRememberMeServices
实现
protected void onLoginSuccess (HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); this .logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this .generateSeriesData(), this .generateTokenData(), new Date()); try { this .tokenRepository.createNewToken(persistentToken); this .addCookie(persistentToken, request, response); } catch (Exception var7) { this .logger.error("Failed to save persistent token " , var7); } }
this.tokenRepository.createNewToken(persistentToken)
通过tokenRepository
,创建新Token,然后通过cookie传回给浏览器,另一边写入数据库做备份。
在 public class JdbcTokenRepositoryImpl
中会对数据库进行操作,让Token存入数据库中。
「记住我」之后,下次的请求过程是
先被 RememberMeAuthenticationFilter
过滤器过滤,其中的 doFilter
方法会完成自动登录
public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (SecurityContextHolder.getContext().getAuthentication() == null ) { Authentication rememberMeAuth = this .rememberMeServices.autoLogin(request, response);
public interface RememberMeServices { Authentication autoLogin (HttpServletRequest var1, HttpServletResponse var2) ;
实现类 AbstractRememberMeServices
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final Authentication autoLogin (HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = this .extractRememberMeCookie(request); if (rememberMeCookie == null ) { return null ; } else { this .logger.debug("Remember-me cookie detected" ); if (rememberMeCookie.length() == 0 ) { this .logger.debug("Cookie was empty" ); this .cancelCookie(request, response); return null ; } else { UserDetails user = null ; try { String[] cookieTokens = this .decodeCookie(rememberMeCookie); user = this .processAutoLoginCookie(cookieTokens, request, response); this .userDetailsChecker.check(user); this .logger.debug("Remember-me cookie accepted" ); return this .createSuccessfulAuthentication(request, user);
前面判空。然后 try 里头对cookie解码,再调用 processAutoLoginCookie
在实现类中 PersistentTokenBasedRememberMeServices
protected UserDetails processAutoLoginCookie (String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { .... return this .getUserDetailsService().loadUserByUsername(token.getUsername());
调用 loadUserByUsername
,即接口 UserDetailsService
里的实现类调用此方法进行数据库的查询,返回之前在数据库中存的cookie,然后进行 check
比对,成功则直接return,createSuccessfulAuthentication
成功认证。
5.9 CSRF
<!--对Thymeleaf添加Spring Security标签支持--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</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-thymeleaf</artifactId> </dependency>
<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
「原理」
生成 csrfToken 保存到 HttpSession 或者 Cookie 中
public interface CsrfToken extends Serializable { String getHeaderName () ; String getParameterName () ; String getToken () ; }
SaveOnAccessCsrfToken
类有个接口 CsrfTokenRepository
private static final class SaveOnAccessCsrfToken implements CsrfToken { private transient CsrfTokenRepository tokenRepository;
其实现类 HttpSessionCsrfTokenRepository
CookieCsrfTokenRepository
CookieCsrfTokenRepository
截取部分
public final class CookieCsrfTokenRepository implements CsrfTokenRepository { private String parameterName = "_csrf" ; public CsrfToken generateToken(HttpServletRequest request ) { return new DefaultCsrfToken(this .headerName , this .parameterName , this .createNewToken () ); } private String createNewToken() { return UUID . randomUUID() .to String() ; } }
请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter
过滤器来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final class CsrfFilter extends OncePerRequestFilter { private final CsrfTokenRepository tokenRepository; protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this .tokenRepository.loadToken(request); boolean missingToken = csrfToken == null ; if (missingToken) { csrfToken = this .tokenRepository.generateToken(request); this .tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); if (!this .requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); } ...
基于微服务和jwt待续。。。
鸣谢:
https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#servlet-hello
https://www.bilibili.com/video/BV1VE411h7aL
https://www.bilibili.com/video/BV15a411A7kP?from=search&seid=12610921816944973603