一,简介
spring security的核心功能包括:
-
认证(你是谁)
-
授权(你能干什么)
-
攻击防护(防止伪造身份)
其核心就是一组过滤链,项目启动后会自动配置。最核心的就是Basic Authenitication Filter 用来认证用户身份,一个在spring security中一种过滤器处理一种认证方式。
比如,对username,password认证过滤器来说
-
会检查是否是一个登录请求
-
是否包含username和password(也就是该过滤器需要对的一些认证信息)
-
如果不满足则放行给下一个。
下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic eHh4Onh4 的信息。中间可能还有更多的认证过滤器。最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。
注意:绿色的过滤器可以配置是否生效,其他的都不能控制。
二,第一个项目
1,导入依赖
Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可。
<!-- 导入spring security依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2,访问页面
导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,则跳转内置登录页面。
在项目中新建login.html后
在浏览器输入:http://localhost:8080/login.html后会显示下面页面。
3,UserDetailsService详解
当什么也没配置时候,账号和密码都是由Spring Security定义生成的。而在实际项目中的账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现UserDetailsService接口即可,接口定义如下:
public interface UserDetailsService {
//通过loadUserByUsername来验证登录逻辑
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(); //判断是否可用
}
方法如下
方法详解:
1:Collection<? extends GrantedAuthority> getAuthorities();
获取用户权限,不能返回null;
2:String getPassword();
获取密码
3:String getUsername();
获取用户名
4:boolean isAccountNonExpired();
判断账户是否未过期
5:boolean isAccountNonLocked();
判断账户是否未被锁定
6:boolean isCredentialsNonExpired();
判断凭证(密码)是否未过期
7:boolean isEnabled();
账户是否启用
要想返回UserDetails的实例,就只能返回接口的实现类。Spring Security 中提供了如下实例。对于我们只需要使用里面的User类即可。User是UserDetails的实现类。要注意User的全限定路径是:
org.springframework.security.core.userdetails.User
这里常会和系统自己开发的User类弄混。
在user类中提供了很多方法和属性。
其中构造方法有两个,调用其中任何一个都可以实例化
UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也
是调用 7 个参数的构造方法。
username:用户名
password:密码
authorities:用户具有的权限。此处不允许为 null
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
此处的用户名应该是客户端传递过来的用户名。而密码应该是从
数据库中查询出来的密码。Spring Security 会根据 User 中的 password
和客户端传递过来的 password 进行比较。如果相同则表示认证通过,
如果不相同表示认证失败。
authorities里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户的权限,如果里面没有包含某个权限,而在做某件事的时候必须包含某个权限则会出现403.通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”)来创建authorities集合对象的,参数是用一个字符串。多个权限使用逗号来分割。
方法参数
方法参数表示用户名,此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无法接收。
4,passwordEncoder密码解析器详解
Spring Security要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PasswordEncoder的bean对象。
接口介绍
encode():把参数按照特定的解析规则进行解析(加密)。
matches()验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示储存的密码。
boolean matches(CharSequence var1, String var2);//var1为客户端传给我们的密码,var2为加密后的密码 两个进行匹配,能配上就返回true,否则返回false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
} // 2次加密 ,能2次加密true,不能false
UpgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则false。默认为false。
简单演示
@Test
public void password(){
PasswordEncoder ps =new BCryptPasswordEncoder();
//加密
String encode = ps.encode("231");
System.out.println(encode);
//比较密码
boolean matches = ps.matches("123", encode);
System.out.println(matches);
}
//输出之后为:$2a$10$56mEklf6Vfnug4EAYdoXl.XnGrSHQQlPLatviAInA529dnU40heLO
BCryptPasswordEncoder简介
BCryptPasswordEncoder是Spring Security官方推荐的密码解析器。
BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认为10.
代码演示
@Test
public void test(){
//创建解析器
PasswordEncoder encoder =new BCryptPasswordEncoder();
//对密码进行加密
String password =encoder.encode("123");
System.out.println("加密的密码:"+password);
//判断原字符加密后的内容是否匹配
boolean result =encoder.matches("1234",password);
System.out.println("判断是否一致:"+result);
}
}
5,自定义登录逻辑
进行自定义登录逻辑需要用到之前的UserDetailsService和PasswordEncoder。但是 Spring Security要求:当自定义登录逻辑时,容器内必须有PasswordEncoder实例。所以不能直接new对象。
编写配置类
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPwdEncoder(){
return new BCryptPasswordEncoder();
}
}
自定义逻辑
在spring Security中实现UserDetailService就表示为客户详情服务。在这个类中编写用户认证逻辑。
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库判断用户名是否存在,如果不存在则抛出UsernameNoFoundException异常。
if (!username.equals("admin")){ //后面更改为自定义的值或从数据库查询的值
throw new UsernameNotFoundException("用户名不存在");
}
//把查询出来的密码进行解析,或直接把password放入构造方法中
//password就是从数据库中查询出来的密码
String password =passwordEncoder.encode("123456");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
6,自定义登录页面
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写configure方法。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//表单认证
http.formLogin()
//loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。
.loginProcessingUrl("/login") //当发现/login时,认为是登录,需要执行UserDetailsServiceImpl
.successForwardUrl("/toMain") //此处是post请求,successForwardUrl()登录成功后跳转地址
.loginPage("/login.html") ; //loginPage登录页面
//url 拦截
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //login.html 不需要被验证 antMatchers():匹配内容 permitAll():允许
.anyRequest().authenticated(); //所有请求必须被认证,必须登录后才能访问
//关掉csrf防护
http.csrf().disable();
}
编写控制器
当用户登录成功后跳转toMain控制器(而之前的/login 控制器方法是不执行的,所以可以直接删除)
@Controller
public class LoginController {
@PostMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
}
失败跳转
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
操作失败,请重新登录.
<a href="/login.html">跳转</a>
</body>
</html>
修改失败跳转的表单配置
对fail跳转不进行拦截
添加控制器方法
@PostMapping("fail")
public String fail(){
return "redirect:/fail.html";
}
7,扩展:请求账户和密码的参数名
当进行登录时会执行UsernamePasswordAuthenticationFilter过滤器。
修改配置文件
修改后前端页面中form表单中的name也要修改。
控制器
使用successForwardUrl()时表示成功后转发请求到地址。内部是通过success Handler()方法进行控制成功后交给哪个类进行处理。
public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
return this;
}
ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或者在前后端分离的项目中就无法使用。
所以当控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。
自定义成功控制器
自定义类
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//Principal主体。存放了登录用户的信息
Users users= (Users) authentication.getPrincipal();
System.out.println(users.getUsername());
System.out.println(users.getPassword());
httpServletResponse.sendRedirect("http://www.baidu.com");
}
}
修改配置项
使用successHandler()方法设置成功后交给哪个对象进行处理
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//表单认证
http.formLogin()
//loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。
.loginProcessingUrl("/login") //当发现/login时,认为是登录,需要执行UserDetailsServiceImpl
.successHandler(new MyAuthenticationSuccessHandler())
// .successForwardUrl("/toMain") //此处是post请求,successForwardUrl()登录成功后跳转地址
.failureForwardUrl("/fail") //当登录失败时进行跳转
.loginPage("/login.html") ; //loginPage登录页面
同理也可以自定义失败处理器
自定义失败处理器
源码
public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
this.failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
return this;
}
点进去
public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String forwardUrl;
public ForwardAuthenticationFailureHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
this.forwardUrl = forwardUrl;
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//request作用域中设置了存储的异常对象 request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
自定义失败处理
public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("/fail.html");
}
}
修改配置文件
三,安全深入详解
1,访问控制url匹配
在配置类中Http.authorizeRequests()主要是对url进行控制,也就是我们说的授权(访问控制)。
在所有的匹配规则中取所有规则的交集。配置顺序影响了之后的授权效果,越是具体的配置就应该放到前面。越笼统的应该放到后面。
anyRequest()
表示匹配所有的请求,设置:全部内容都需要进行认证。
antMatcher()
public C antMatchers(String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return this.chainRequestMatchers(AbstractRequestMatcherRegistry.RequestMatchers.antMatchers(antPatterns));
}
参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。
规则如下:
? 匹配一个字符
* 匹配0个或多个字符
** 匹配0个或多个目录
放行js文件夹下所有的脚本文件:.antMatchers("/js/**").permitAll()
放行只要是".js"的文件: antMatchers("/**/*.js").permitAll()
regexMatchers
使用正则表达式进行匹配。和antMatchers()主要的区别是参数,antMatchers()参数是ant表达式,regexMatchers()参数是正则表达式。
以.js结尾的文件都被放行:.regexMatchers(".+[.]js").permitAll()
两个参数时使用方式
无论是antMatchers()还是regexMatchers()都具有两个参数的方法,其中第一个参数都是HttpMethod,表示请求方式。当被设置后,只有特定的请求方式才执行的对应的权限设置。
public C antMatchers(String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return this.chainRequestMatchers(AbstractRequestMatcherRegistry.RequestMatchers.antMatchers(antPatterns));
}
mvcMatchers()
mvcMatchers()适用于配置了servletPath的情况。
servletPath就是所有的URL的统一前缀。在Spring整合SpringMVC的项目中可以在yml文件中添加:spring.mvc.servlet.path= /bjsxt
此时的访问控制可以写成:.mvcMatchers("demo").servletPath("/bjsxt").permitAll()
或者:antMatchers("/bjsxt/demo").permitAll()
2,内置访问控制方法介绍
Spring Security 匹配了URL后调用了permitAll()表示不需要认证,随意访问。
permitAll()表示所匹配的URL能被所有人访问
authenticated()表示所匹配的URL都需要被认证才能访问。
anonymous()表示可以匿名访问匹配的URL。
denyAll()表示所匹配的URL都不允许被访问。
rememberMe() 表示被"remember me"的用户允许访问
fullyAuthenticated() 如果用户不是被remember me 的,才能访问。
3,权限判断
用于判断用户是否具有特定的要求
hasAuthority(String)
判断用户是否具有特定的权限,用户权限是在自定义登录逻辑中创建User对象时指定的。
下图的admin就是用户的权限,需要严格区分大小写。
在配置类中通过hasMatchers("admin")设置具有admin权限时才能访问。
.antMatchers("/main1.html").hasAuthority("admin")
hasAnyAuthority(String...)
如果用户具备给定权限中的某一个,就允许访问。
给的权限严格区分大小写。
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
hasRole(String)
如果用户具备给定角色就允许访问。否则出现403.
参数取值源于自定义登录逻辑UserDetailsService实现类中创建User对象时给User对象时给User赋予的授权。
在给用户赋予角色时角色需要以ROLE开头,后面添加角色名称。例如ROLE_abc 其中abc是角色名,ROLE是固定字符开头。使用hasRole()时参数也只用写abc即可。否则会报错。
hasAnyRole(String...)
如果用户具备给定角色的任意一个,就允许被访问。
hasIpAddress(String)
如果请求是指定的IP就运行访问。
可以通过request.getRemoteAddr()获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip 地址是不一样的
四,自定义403异常处理
403异常表示无权限异常,针对此异常应当给别人看到我们自定义的画面提示,而非程序员专用画面
1,新建类AccessDeniedHandler
@Component //表示此类为一个bean
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
//SC_FORBIDDEN表示403的异常状态码
httpServletResponse.setStatus(httpServletResponse.SC_FORBIDDEN);
//设置返回的格式
httpServletResponse.setHeader("Content-Type","application/json;charset/utf-8");
PrintWriter out =httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
out.flush();
out.close();
}
}
2,修改配置类
配置类中重点添加异常处理器,设置访问受限后交给哪个对象进行处理。
MyAccessDeniedHandler在配置类中自动注入
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
五,底层源码分析
security 本质上是一个过滤链
1,UsernamePasswordAuthenticationFilter 过滤
字面概要:登录请求过滤:UsernamePasswordAuthenticationFilter中判断是否为post请求,只要是post请求都会被拦截到,让你进行身份验证。如果是post请求,在父类AbstractAuthenticationprocessingFilter中调用子类方法进行身份验证(查数据库,返回userDtils并封装到Authentication中,并且做session策略设置),失败做失败方法,成功进行成功的方法。