一、什么是springsecurity?
SpringSecurity是Spring家族的成员之一,SpringSecurity基于Spring框架,提供了一整套Web应用安全性的解决方案。
一般情况来说,Web应用的安全性包括两部分,分别是:用户认证(Authentication)和用户授权(Authorization),这两点也是SpringSecurity的重要核心功能。
用户认证(Authentication):通俗的讲,就是验证用户是否可以成功登陆系统。
用户授权(Authorization):通俗的讲,就是验证用户是否具有访问某些资源的权限。
二、基本概念。
1、用户认证(Authentication)
什么是认证?
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
为什么需要认证?
认证的作用是为了保护系统的隐私数据与资源的安全性,用户的身份合法才可访问该系统的资源,比如您现在购买完商品付钱的时候,“钱”属于自己隐私就需要认证。
2、什么是授权(Authorization)
就拿支付宝来举例子,支付宝登陆成功后用户就可以使用转账、发红包、花呗、添加好友等功能,没有绑定银行卡是不可以转账的,花呗属于支付宝的资源,你需要向支付宝申请此功能才能去使用,当你申请成功才能拥有花呗功能,这个根据用户使用资源就是授权。
为什么需要授权?
认证是保证了用户的合法性,而授权是为了细粒度的控制用户使用的“资源”,控制不同的用户使用不同的资源。
3、springsecurity的授权模型。
授权模型RBAC(Role-Based Access Control:基于角色的访问控制),基于角色来进行访问权限控制。
三、SpringSecurity和Shiro的区别。
1、Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下。
2、相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
3、自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方 案,可以使用更少的配置来使用 Spring Security。
SpringSecurity | Shiro | |
组织 | Spring成员之一,可以和Spring框架无缝整合 | Apache旗下的权限控制框架 |
通用性 | 旧版不能脱离Web环境使用。 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境使用。 | 不局限于Web环境,可以脱离Web环境使用。 |
重量级框架 | 轻量级框架 | |
整合方案 | SpringBoot/SpringCloud | SSM |
Ps:关于整合方案,只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。
四、Hello SpringSecurity
1、创建一个web项目,并且集成SpringSecurity
2、添加一个配置类
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
3、添加一个测试类
package com.example.demo.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello world !";
}
@GetMapping("/index")
public String index(){
return "welcome!";
}
}
4、测试
项目运行起来之后,控制台会生成一个密码,我们用这个密码登陆,用户名默认是user
浏览器访问http://localhost:8080/test/hello,会自动跳转到登陆页面进行登陆
登陆成功后(用户通过认证),可以正常请求接口地址
五、用户认证(Authentication)
SpringSecurity设置用户名和密码,提供了两种常用的配置方式,分别是:
第一种:通过配置文件
第二种:自定义编写实现类
1、通过配置文件,在application.yml中进行配置
spring:
security:
user:
name: gao
password: 123123
2、自定义编写实现类
当什么都没有配置的时候,账号和密码是由SpringSecurity自动生成的。而在实际的项目开发中,账号和密码都是从数据库中查询出来的。所以,我们可以通过自定义逻辑控制认证。
如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
定义MyUserDetailsService类,通过@Service注解,按照名称将SecurityConfig类中的UserDetailsService对象的名称注入。
package com.example.demo.security;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//用户具备哪些权限,一般从数据库中查询
List<GrantedAuthority> atuhs = AuthorityUtils.commaSeparatedStringToAuthorityList("manager,common");
return new User("admin", new BCryptPasswordEncoder().encode("123"),atuhs);
}
}
实际开发中,我们一般会通过数据库来认证用户信息,大概实现思路是在自定义UserDetailsService类中的loadUserByUsername方法中根据用户名去数据库中查询用户信息,如果满足要求,则用户通过认证,如果不满足要求,则抛出一个UsernameNotFoundException异常,该异常时SpringSecurity内部定义的用于抛出用户不存在的异常。
六、用户授权(Authorization)
- hasAuthority方法:如果当前的主体具有指定的权限,则返回true,否则返回false。
- hasAnyAuthority方法:如果当前的主体有任何提供的权限的话,则返回true,否则返回false。
Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝
在SecurityConfig配置类中设置访问资源的权限的逻辑
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
在MyUserDetailsService类中添加授权的逻辑(用户被授予的权限)
package com.example.demo.security;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//用户具备哪些权限,一般从数据库中查询
List<GrantedAuthority> atuhs = AuthorityUtils.commaSeparatedStringToAuthorityList("manager,common");
return new User("admin", new BCryptPasswordEncoder().encode("123"),atuhs);
}
}
2、基于角色进行访问控制
hasRole方法:如果用户具有指定角色,则返回true,否则false。
hasAnyRole方法:如果用户具有指定的任意角色,则返回true,否则返回false。
Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝
在SecurityConfig配置类中设置访问资源的角色的逻辑
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
在MyUserDetailsService类中添加授权的逻辑(用户被授予的角色)
package com.example.demo.security;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//用户具备哪些权限,一般从数据库中查询
List<GrantedAuthority> atuhs = AuthorityUtils.commaSeparatedStringToAuthorityList("manager,common");
return new User("admin", new BCryptPasswordEncoder().encode("123"),atuhs);
}
}
七、自定义页面
1、自定义页面
在resources/static目录下定义一个访问被拒的页面unauth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>访问被拒绝,您没有权限浏览该页面</h3>
</body>
</html>
在SecurityConfig配置文件中进行配置
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
2、自定义登陆页面
在resources/static目录下定义一个访问被拒的页面login.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名<input type="text" name="username"></br>
密码<input type="password" name="password"></br>
<input type="submit" value="登录">
</form>
</body>
</html>
在SecurityConfig配置文件中进行配置
package com.example.demo.security;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//认证及授权的配置
//配置没有权限访问时(403),跳转的自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()//基于表单登录
.loginPage("/login.html")//登录页面
.loginProcessingUrl("/user/login")//登录访问路径
.defaultSuccessUrl("/test/hello").permitAll()//登录成功之后跳转的路径
.and()
.authorizeRequests()//认证配置
//设置哪些路径可以访问,不需要认证
.antMatchers("/test/hello").permitAll()
//当前登录用户只有具备admin权限时才可以访问这个路径
//.antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户只有具备admin和manager任意权限,就可以访问这个路径
//.antMatchers("/test/index").hasAnyAuthority("admin","manager")
//当前登录的用户只有具备sale角色时,才可以访问这个路径
//.antMatchers("/test/index").hasRole("sale")
//当前登录的用户,只要具备sale或common任一角色,就可以访问资源
.antMatchers("/test/index").hasAnyRole("sale","common")
.anyRequest()//所有的请求
.authenticated()//都需要身份验证
.and().csrf().disable();//关闭跨域拦截
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
3、用户注销
在resources/static目录下定义一个访问被拒的页面success.html,当用户登陆成功后进入到这个页面,该页面提供一个用户注销的按钮。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>登录成功</h3>
<br/>
<a href="/logout">退出登录</a>
</body>
</html>
在SecurityConfig配置文件中进行配置
package com.example.demo01.configuration;
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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//退出登录
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
...
http.formLogin() //表单登陆
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
// .defaultSuccessUrl("/test/index").permitAll() //登录成功之后跳转路径
.defaultSuccessUrl("/success.html").permitAll() //登录成功之后跳转成功页面,配合退出登录
.and()
...
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
八、扩展 :SpringSecurity的三个重要过滤器
Spring Security本质是一个过滤器链,有三个重要过滤器,分别是:
FilterSecurityInterceptor
ExceptionTranslationFilter
UsernamePasswordAuthenticationFilter
1、FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
2、ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
this.rethrow(var8);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
}
}
3、UsernamePasswordAuthenticationFilter:对/login 的 POST 请求做拦截,校验表单中用户,密码。
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);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
九、扩展:PasswordEncoder接口:用来加密数据
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,是对bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认10。