SpringSecurity入门到入土教程_1
https://gitee.com/fakerlove/spring-security
文章目录
- SpringSecurity 教程
- 1. 简介
- 1.1 概念
- 1.2 入门案例
- 1.3 自定义登录逻辑
- 1.4 自定义登录页面
- 1.5 自定用户名参数
- 1.5 自定义成功处理器
- 1.6 登录失败处理器
- 1.7 认证
- anyRequest
- antMatchers
- regexMatchers
- 1.8 授权
- 基于权限配置
- 基于角色权限配置
- 基于ip 地址
- 基于access 权限配置
- 1.9 注解
- @Secured
- @PreAuthorize
- 2. Security 原理分析
- 2.1 SpringSecurity 过滤器链
- 2.2 SpringSecurity 流程图
- 流程说明
- 3. Security 配置
- 3.1 配置类伪代码
- 3.2 配置简介
- 4. Security 权限系统
- 5. Security 扩展
- 6. 总结
SpringSecurity 教程
1. 简介
1.1 概念
spring security 的核心功能主要包括:
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
1.2 入门案例
创建一个maven 项目
添加pom 文件
<?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>org.example</groupId><artifactId>SpringSecurityDemo</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId><version>2.4.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.4.1</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>2.4.1</version></dependency></dependencies> </project>
创建application.yml
server:port: 8082 spring:mvc:view:prefix: /templates/
创建controller
package com.ak.demo.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller public class LoginDemo {@RequestMapping("/mylogin")public String login(){return "login";} }
创建 登录页面
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title> </head> <body> 用户名 <input type="text" ><br> 密码 <input type= "password"> </body> </html>
输入登录地址
http://localhost:8082/mylogin 会被拦截出现这个画面
输入user 用户名
密码在控制台
实现的时候是使用了
在UserDetailsService 里面 验证是否有次用户,
验证密码的过程在这里
package com.ak.test;import com.ak.demo.Demo8082Application; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;@SpringBootTest(classes = Demo8082Application.class) public class MyTest {@Testpublic void Test(){PasswordEncoder encoder=new BCryptPasswordEncoder();String pass=encoder.encode("123");System.out.println(pass);boolean matches=encoder.matches("123", pass);System.out.println(matches);} }
1.3 自定义登录逻辑
package com.ak.demo.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;@Service
public class LoginService implements UserDetailsService {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@AutowiredPasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 自定义 用户登录名if(!username.equals("admin")){throw new UsernameNotFoundException("这是俺自己写的错误");}// 自定义用户密码String password=passwordEncoder.encode("123");return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));}
}
1.4 自定义登录页面
登录页面
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title> </head> <body> <form action="/login" method="post">用户名 <input type="text" name="username"><br>密码 <input type= "password" name="password"><input type="submit" value="提交"> </form></body> </html>
自定义登录页面 name 必须是username,password ,不然就会报错。
如果不想写 username ,就需要配置
配置类
package com.ak.demo.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller;@Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password.successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.anyRequest().authenticated();http.csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();} }
controller 层
package com.ak.demo.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller public class LoginDemo {@RequestMapping("/myLoginController")public String myLogin(){return "myLogin";}@RequestMapping("/toSuccess")public String toSuccess(){return "success";} }
输入
http://localhost:8082/myLoginController
输入admin ,123 ,自定义配置文件
1.5 自定用户名参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NqHkQULK-1609335900031)()]
这两个地方一定要一致
1.5 自定义成功处理器
package com.ak.demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Controller;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// .usernameParameter("username123")
// .passwordParameter("password123").loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password// .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面.successHandler(new AuthenticationSuccessHandler() {@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.sendRedirect("https://www.baidu.com");}});http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.anyRequest().authenticated();http.csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
登录完毕后,就是自动跳转到 百度
1.6 登录失败处理器
package com.ak.demo.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class LoginDemo {@RequestMapping("/myLoginController")public String myLogin(){return "myLogin";}@RequestMapping("/toSuccess")public String toSuccess(){return "success";}@RequestMapping("/toFail")public String toFail(){return "fail";}
}
失败页面
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title> </head> <body> 失败的页面 <a href="/myLoginController">返回登录</a> </body> </html>
配置类
package com.ak.demo.config;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler; import org.springframework.stereotype.Controller;import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;@Configuration public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() // .usernameParameter("username123") // .passwordParameter("password123").loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password// .successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面.successHandler(new AuthenticationSuccessHandler() {@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.sendRedirect("https://www.baidu.com");}}).failureHandler(new ForwardAuthenticationFailureHandler("/toFail"));http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.anyRequest().authenticated();http.csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();} }
1.7 认证
anyRequest
所有请求
antMatchers
任何匹配的请求
regexMatchers
正则匹配请求
1.8 授权
基于权限配置
修改页面
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title> </head> <body> 登录成功的主页面<br> <a href="/Vip1">拥有vip1 才能看的</a><br> <a href="/Vip2">拥有vip2 才能看的</a></body> </html>
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title> </head> <body>这是 vip1 角色才能看见的信息 </body> </html>
修改Service
package com.ak.demo.service;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service;@Service public class LoginService implements UserDetailsService {@AutowiredPasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 自定义 用户登录名if(!(username.equals("admin")||username.equals("vip1")||username.equals("vip2"))){throw new UsernameNotFoundException("这是俺自己写的错误");}if(username.equals("vip1")){// 自定义用户密码String password=passwordEncoder.encode("123");return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,vip1"));}if(username.equals("vip2")){// 自定义用户密码String password=passwordEncoder.encode("123");return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,vip2"));}// 自定义用户密码String password=passwordEncoder.encode("123");return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));} }
现在有三个用户
vip1,vip2,admin.
vip1.html 只能 vip1看
vip2 只能vip2 看
admin 看不了 vip1,vip2页面
基于角色权限配置
package com.ak.demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.ForwardAuthenticationFailureHandler;
import org.springframework.stereotype.Controller;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// .usernameParameter("username123")
// .passwordParameter("password123").loginPage("/myLoginController") // 一定要和 Controller 中 返回 myLogin页面 一致,.loginProcessingUrl("/login")// 必须和表单提交 action 的名字 一样的,提交 username 和password.successForwardUrl("/toSuccess");// 这个是 登录成功后返回的界面
// .successHandler(new AuthenticationSuccessHandler() {// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {// httpServletResponse.sendRedirect("https://www.baidu.com");
// }
// }).failureHandler(new ForwardAuthenticationFailureHandler("/toFail"));http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.antMatchers("/Vip1").hasAuthority("vip1").antMatchers("/Vip2").hasAuthority("vip2").antMatchers("/**/*.png").permitAll().antMatchers("/toRole").hasRole("abc").anyRequest().authenticated();http.csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
修改页面
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title> </head> <body> 登录成功的主页面<br> <a href="/Vip1">拥有vip1 才能看的</a><br> <a href="/Vip2">拥有vip2 才能看的</a> <br> <a href="/toRole">拥有abc 角色的人才能访问</a> </body> </html>
基于ip 地址
http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.antMatchers("/Vip1").hasAuthority("vip1").antMatchers("/Vip2").hasAuthority("vip2").antMatchers("/**/*.png").permitAll().antMatchers("/toRole").hasRole("abc").antMatchers("/toRole").hasIpAddress("127.0.0.1").anyRequest().authenticated();
基于access 权限配置
static final String permitAll = "permitAll";private static final String denyAll = "denyAll";private static final String anonymous = "anonymous";private static final String authenticated = "authenticated";private static final String fullyAuthenticated = "fullyAuthenticated";private static final String rememberMe = "rememberMe";
一共就这几种
http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.antMatchers("/Vip1").hasAuthority("vip1").antMatchers("/Vip2").hasAuthority("vip2").antMatchers("/**/*.png").permitAll().antMatchers("/toRole").hasRole("abc").antMatchers("toRole").access("hasRole('abc')")//.antMatchers("/toRole").hasIpAddress("127.0.0.1").anyRequest().authenticated();
1.9 注解
@Secured
判断用户是否由此角色
package com.ak.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;/*** 用户类*/
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Demo8082Application {public static void main(String[] args) {SpringApplication.run(Demo8082Application.class, args);}}
- 修改 配置类
http.authorizeRequests().antMatchers("/myLoginController").permitAll()// 放行myLoginController.anyRequest().authenticated();
修改 Controller
@Secured("ROLE_abc")@RequestMapping("/toRole")public String toRole(){return "Role";}
@PreAuthorize
判断是否拥有权限
修改application
package com.ak.demo;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;/*** 用户类*/ @SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class Demo8082Application {public static void main(String[] args) {SpringApplication.run(Demo8082Application.class, args);}}
修改controller
package com.ak.demo.controller;import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller public class LoginDemo {@RequestMapping("/myLoginController")public String myLogin(){return "myLogin";}@RequestMapping("/toSuccess")public String toSuccess(){return "success";}@RequestMapping("/toFail")public String toFail(){return "fail";}@PreAuthorize("hasAuthority('vip1')")@RequestMapping("/Vip1")public String toVip1(){return "vip1";}@PreAuthorize("hasAuthority('vip2')")@RequestMapping("/Vip2")public String toVip2(){return "vip2";}// @Secured("ROLE_abc")@PreAuthorize("hasRole('abc')")@RequestMapping("/toRole")public String toRole(){return "Role";} }
2. Security 原理分析
2.1 SpringSecurity 过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:
- WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
- HeaderWriterFilter:用于将头信息加入响应中。
- CsrfFilter:用于处理跨站请求伪造。
- LogoutFilter:用于处理退出登录。
- UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自
/login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为username
和password
,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。- DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
- BasicAuthenticationFilter:检测和处理 http basic 认证。
- RequestCacheAwareFilter:用来处理请求的缓存。
- SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
- AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
- SessionManagementFilter:管理 session 的过滤器
- ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
- FilterSecurityInterceptor:可以看做过滤器链的出口。
- RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
2.2 SpringSecurity 流程图
先来看下面一个 Spring Security 执行流程图,只要把 SpringSecurity 的执行过程弄明白了,这个框架就会变得很简单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iOLNTQAe-1609335900035)(picture/p)]
流程说明
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
3. Security 配置
在 WebSecurityConfigurerAdapter
这个类里面可以完成上述流程图的所有配置
3.1 配置类伪代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/resources/**/*.html", "/resources/**/*.js"); } @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/login_page") .passwordParameter("username") .passwordParameter("password") .loginProcessingUrl("/sign_in") .permitAll() .and().authorizeRequests().antMatchers("/test").hasRole("test") .anyRequest().authenticated().accessDecisionManager(accessDecisionManager()) .and().logout().logoutSuccessHandler(new MyLogoutSuccessHandler()) .and().csrf().disable(); http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class); http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler()); http.addFilterAfter(new MyFittler(), LogoutFilter.class); }
}
3.2 配置简介
- configure(AuthenticationManagerBuilder auth)
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
- configure(WebSecurity web)
这个配置方法用于配置静态资源的处理方式,可使用 Ant 匹配规则。
- configure(HttpSecurity http)
这个配置方法是最关键的方法,也是最复杂的方法。我们慢慢掰开来说:
http
.formLogin()
.loginPage("/login_page")
.passwordParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/sign_in")
.permitAll()
这是配置登录相关的操作从方法名可知,配置了登录页请求路径,密码属性名,用户名属性名,和登录请求路径,permitAll()代表任意用户可访问。
http
.authorizeRequests()
.antMatchers("/test").hasRole("test")
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
以上配置是权限相关的配置,配置了一个 /test
url 该有什么权限才能访问, anyRequest() 表示所有请求,authenticated() 表示已登录用户才能访问, accessDecisionManager() 表示绑定在 url 上的鉴权管理器
为了对比,现在贴出另一个权限配置清单:
http.authorizeRequests()
.antMatchers("/tets_a/**","/test_b/**").hasRole("test")
.antMatchers("/a/**","/b/**").authenticated()
.accessDecisionManager(accessDecisionManager())
我们可以看到权限配置的自由度很高,鉴权管理器可以绑定到任意 url 上;而且可以硬编码各种 url 权限:
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
登出相关配置,这里配置了登出 url 和登出成功处理器:
http
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
上面代码是配置鉴权失败的处理器。
http.addFilterAfter(new MyFittler(), LogoutFilter.class);
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
上面代码展示如何在过滤器链中插入自己的过滤器,addFilterBefore 加在对应的过滤器之前,addFilterAfter 加在对应的过滤器之后,addFilterAt 加在过滤器同一位置,事实上框架原有的 Filter 在启动 HttpSecurity 配置的过程中,都由框架完成了其一定程度上固定的配置,是不允许更改替换的。根据测试结果来看,调用 addFilterAt 方法插入的 Filter ,会在这个位置上的原有 Filter 之前执行。
注:关于 HttpSecurity 使用的是链式编程,其中 http.xxxx.and.yyyyy
这种写法和 http.xxxx;http.yyyy
写法意义一样。
- 自定义 AuthenticationManager 和 AccessDecisionManager
重写 authenticationManagerBean() 方法,并构造一个 authenticationManager:
@Override
public AuthenticationManager authenticationManagerBean() throws Exception { ProviderManager authenticationManager = new ProviderManager(Arrays.asLis(getMyAuthenticationProvider(),daoAuthenticationProvider())); return authenticationManager;
}
我这里给 authenticationManager 配置了两个认证器,执行过程参考流程图。
定义构造AccessDecisionManager的方法并在配置类中调用,配置参考 configure(HttpSecurity http) 说明:
public AccessDecisionManager accessDecisionManager(){ List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList( new MyExpressionVoter(), new WebExpressionVoter(), new RoleVoter(), new AuthenticatedVoter()); return new UnanimousBased(decisionVoters);
}
投票管理器会收集投票器投票结果做统计,最终结果大于等于0代表通过;每个投票器会返回三个结果:-1(反对),0(通过),1(赞成)。
4. Security 权限系统
- UserDetails
Security 中的用户接口,我们自定义用户类要实现该接口。
- GrantedAuthority
Security 中的用户权限接口,自定义权限需要实现该接口:
public class MyGrantedAuthority implements GrantedAuthority { private String authority;
}
authority 表示权限字段,需要注意的是在 config 中配置的权限会被加上 ROLE_
前缀,比如我们的配置 authorizeRequests().antMatchers("/test").hasRole("test")
,配置了一个 test
权限但我们存储的权限字段(authority)应该是 ROLE_test
。
- UserDetailsService
Security 中的用户 Service,自定义用户服务类需要实现该接口:
@Service
public class MyUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return..... }
}
loadUserByUsername的作用在上文中已经说明,就是根据用户名查询用户对象。
- SecurityContextHolder
用户在完成登录后 Security 会将用户信息存储到这个类中,之后其他流程需要得到用户信息时都是从这个类中获得,用户信息被封装成 SecurityContext ,而实际存储的类是 SecurityContextHolderStrategy ,默认的SecurityContextHolderStrategy 实现类是 ThreadLocalSecurityContextHolderStrategy 它使用了ThreadLocal来存储了用户信息。
手动填充 SecurityContextHolder 示例:
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("test","test",list);
SecurityContextHolder.getContext().setAuthentication(token);
对于使用 token 鉴权的系统,我们就可以验证token后手动填充SecurityContextHolder,填充时机只要在执行投票器之前即可,或者干脆可以在投票器中填充,然后在登出操作中清空SecurityContextHolder。
5. Security 扩展
Security 可扩展的有
- 鉴权失败处理器
- 验证器
- 登录成功处理器
- 投票器
- 自定义token处理过滤器
- 登出成功处理器
- 登录失败处理器
- 自定义 UsernamePasswordAuthenticationFilter
- 鉴权失败处理器
Security 鉴权失败默认跳转登录页面,我们可以实现 AccessDeniedHandler 接口,重写 handle() 方法来自定义处理逻辑;然后参考配置类说明将处理器加入到配置当中。
- 验证器
实现 AuthenticationProvider 接口来实现自己验证逻辑。需要注意的是在这个类里面就算你抛出异常,也不会中断验证流程,而是算你验证失败,我们由流程图知道,只要有一个验证器验证成功,就算验证成功,所以你需要留意这一点。
- 登录成功处理器
在 Security 中验证成功默认跳转到上一次请求页面或者路径为 “/” 的页面,我们同样可以自定义:继承 SimpleUrlAuthenticationSuccessHandler 这个类或者实现 AuthenticationSuccessHandler 接口。我这里建议采用继承的方式,SimpleUrlAuthenticationSuccessHandler 是默认的处理器,采用继承可以契合里氏替换原则,提高代码的复用性和避免不必要的错误。
- 投票器
投票器可继承 WebExpressionVoter 或者实现 AccessDecisionVoter接口;WebExpressionVoter 是 Security 默认的投票器;我这里同样建议采用继承的方式;添加到配置的方式参考 上文;
注意:投票器 vote 方法返回一个int值;-1代表反对,0代表弃权,1代表赞成;投票管理器收集投票结果,如果最终结果大于等于0则放行该请求。
- 自定义token处理过滤器
自定义 token 处理器继承自 OncePerRequestFilter 或者 GenericFilterBean 或者 Filter 都可以,在这个处理器里面需要完成的逻辑是:获取请求里的 token,验证 token 是否合法然后填充 SecurityContextHolder ,虽然说过滤器只要添加在投票器之前就可以,但我这里还是建议添加在 http.addFilterAfter(new MyFittler(), LogoutFilter.class);
- 登出成功处理器
实现LogoutSuccessHandler接口,添加到配置的方式参考上文。
- 登录失败处理器
登录失败默认跳转到登录页,我们同样可以自定义。继承 SimpleUrlAuthenticationFailureHandler 或者实现 AuthenticationFailureHandler,建议采用继承。
- 自定义UsernamePasswordAuthenticationFilter
我们自定义UsernamePasswordAuthenticationFilter可以极大提高我们 Security的灵活性(比如添加验证验证码是否正确的功能)。
我们直接继承 UsernamePasswordAuthenticationFilter ,然后在配置类中初始化这个过滤器,给这个过滤器添加登录失败处理器,登录成功处理器,登录管理器,登录请求 url 。
这里配置略微复杂,贴一下代码清单
初始化过滤器:
MyUsernamePasswordAuthenticationFilte getAuthenticationFilter(){ MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter(redisService); myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new MyUrlAuthenticationFailureHandler()); myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler()); myUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/sign_in"); myUsernamePasswordAuthenticationFilter.setAuthenticationManager(getAuthenticationManager()); return myUsernamePasswordAuthenticationFilter;
}
添加到配置:
http.addFilterAt(getAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
6. 总结
对于 Security 的扩展配置关键在于 configure(HttpSecurityhttp)
方法;扩展认证方式可以自定义 authenticationManager
并加入自己验证器,在验证器中抛出异常不会终止验证流程;扩展鉴权方式可以自定义 accessDecisionManager
然后添加自己的投票器并绑定到对应的 url(url 匹配方式为 ant)上,投票器 vote(Authenticationauthentication,FilterInvocationfi,Collection<ConfigAttribute>attributes)
方法返回值为三种:-1 0 1,分别表示反对弃权赞成。
对于 token 认证的校验方式,可以暴露一个获取的接口,或者重写 UsernamePasswordAuthenticationFilter
过滤器和扩展登录成功处理器来获取 token,然后在 LogoutFilter
之后添加一个自定义过滤器,用于校验和填充 SecurityContextHolder。
另外,Security 的处理器大部分都是重定向的,我们的项目如果是前后端分离的话,我们希望无论什么情况都返回 json ,那么就需要重写各个处理器了。
SpringSecurity入门到入土教程_1相关推荐
- SpringSecurity入门到入土教程_2 Oauth教程
https://gitee.com/fakerlove/spring-security 文章目录 SpringOauth 教程 1. 简介 1.1 oauth2 概念 架构图 验证流程 spring ...
- Redis入门到入土教程_1
Redis01_下载和安装 Redis (Remote Dictionary Server 远程字典服务) 是一种 C语言编写 的 可基于内存 亦 可持久化 的日志型.Key-Value数据库 官网: ...
- Redis入门到入土教程_2 远程连接redis
文章目录 Redis 教程 1. 去除保护模式 1.1 注释 bind 127.0.0.1 1.2 关闭保护模式 1.3 设置redis 登录密码 1.4 设置后台自动启动 1.5 开放防火墙 :st ...
- python从入门到住院_python从入门到入土教程(1)——入门课
一 前言 1 why python? Python是数据科学界的主流编程语言,相对于传统的Excel等数据处理工具,Python具备处理海量数据的能力,并且可以执行机器学习算法.从数据获取(网络爬虫等 ...
- python控制流教程_python从入门到入土教程(2)——控制流和函数
一 内置函数和库 二 控制流 * if condition_1: statement_block_1 * elif condition_2: statement_block_2 * else: sta ...
- Flink 教程 gitbook 从入门到入土(详细教程)
Flink从入门到入土(详细教程) 和其他所有的计算框架一样,flink也有一些基础的开发步骤以及基础,核心的API,从开发步骤的角度来讲,主要分为四大部分 1.Environment Flink J ...
- flowable画图教程_Flowable从入门到入土(1)-初始Flowable
Flowable从入门到入土 作者:独钓寒江雪 一剑一酒一江湖, 便是此生 心中有图,何必点灯.装载请注明出处 前文传送门: 简介 当前周围好多人都对flowable有所了解乃至投入到生产使用,此文章 ...
- PIXIJS超级详细教程【从入门到入土-下】
来自GitHub教程 GitHub - Zainking/LearningPixi: ⚡️Pixi教程中文版 PIXIJS教程[从入门到入土-上]地址[https://blog.csdn.net/qq ...
- PixiJS超级详细教程【从入门到入土-上】
PixiJS 来自GitHub教程 GitHub - Zainking/LearningPixi: ⚡️Pixi教程中文版 PixiJS超级详细教程[从入门到入土-下]地址[https://blog. ...
最新文章
- 探索“植物人”语言能力和意识水平
- 设计模式---(创建型)单例模式
- MySQL 隐式类型转换导致条件等号的异常
- 【dubbo】http.conn.HttpHostConnectException.host: 'org.apache.http.HttpHost' could not be instantiated
- tgc 什么意思 tgt_TGT的完整形式是什么?
- 度微尔开发者联盟网站
- SWF反编神器Action Script Viewer终身免费升级!
- Sublime Text 2安装汉化破解、插件包安装教程
- 对棋盘完美覆盖问题证明过程的质疑及其解决(续)
- 微软云架构服务器,微软云存储架构(Azure Cloud Storage)
- 百度区块链 xuperchain 如何剪枝 裁剪区块
- VNPY思维导图架构
- java range注解_最全的Java Spring注解
- Xiaojie雷达之路---车载雷达信号处理流程
- 附和导线平差程序(by C#)
- 这世界没有能够预测未来的魔法水晶球
- spss26没有典型相关性分析_SPSS进行典型相关分析结果总结
- 计算机组装后要干什么,电脑组装完后还有哪些事需要干?
- npm ERR network Invalid response body while trying to fetch
- matlab hist 横坐标,matlab中hist函数的用法_matlab中hist函数 将EXCEL 中的日期 时间 导入到MATLAB坐标轴中...
热门文章
- Android5.x(NTP和NITZ)时间同步
- Android Camera之SurfaceView学习
- 深度学习自学(十一):Aborted at 1558257386 (unix time)
- 安全地更改grub2屏幕分辨率
- java路径不存在则创建文件夹_java实现文件夹不存在则创建
- 大厂退场方式-支付宝-相互宝
- 笔记本--摄像头驱动--找不到相机--浏览器打开相机失败--未检测到摄像头--剩下的标题我就不想了--太长了
- python读取url中存储的数据_Python实现从URL地址提取文件名的方法
- telnet发送socket报文_简单讲解一下Socket网络编程
- php 日期 星期_php日期如何转星期