SpringSecurity是Spring家族的成员之一,SpringSecurity基于Spring框架,提供了一整套Web应用安全性的解决方案。
一般情况来说,Web应用的安全性包括两部分,分别是:用户认证(Authentication)和用户授权(Authorization),这两点也是SpringSecurity的重要核心功能。
--什么是认证?
进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。
--为什么需要认证?
认证的作用是为了保护系统的隐私数据与资源的安全性,用户的身份合法才可访问该系统的资源,比如您现在购买完商品付钱的时候,“钱”属于自己隐私就需要认证。
就拿支付宝来举例子,支付宝登陆成功后用户就可以使用转账、发红包、花呗、添加好友等功能,没有绑定银行卡是不可以转账的,花呗属于支付宝的资源,你需要向支付宝申请此功能才能去使用,当你申请成功才能拥有花呗功能,这个根据用户使用资源就是授权。
--为什么需要授权?
认证是保证了用户的合法性,而授权是为了细粒度的控制用户使用的“资源”,控制不同的用户使用不同的资源。
授权模型RBAC(Role-Based Access Control:基于角色的访问控制),基于角色来进行访问权限控制。
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下。
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方 案,可以使用更少的配置来使用 Spring Security。
SpringSecurity | Shiro | |
组织 | Spring成员之一,可以和Spring框架无缝整合 | Apache旗下的权限控制框架 |
通用性 |
|
不局限于Web环境,可以脱离Web环境使用。 |
重量级框架 | 轻量级框架 | |
整合方案 | SpringBoot/SpringCloud | SSM |
Ps:关于整合方案,只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。
package com.example.demo01.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登陆
.and()
.authorizeRequests() //认证配置
.anyRequest() //任何请求
.authenticated(); //都需要身份验证
}
}
package com.example.demo01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RequestMapping("/test")
@RestController
public class Demo01Application {
public static void main(String[] args) {
SpringApplication.run(Demo01Application.class, args);
}
@GetMapping("/hello")
public String helloWorld(){
return "Hello World !";
}
}
项目运行起来之后,控制台会生成一个密码,我们用这个密码登陆,用户名默认是user
浏览器访问http://localhost:8080/test/hello,会自动跳转到登陆页面进行登陆
登陆成功后(用户通过认证),可以正常请求接口地址
SpringSecurity设置用户名和密码,提供了两种常用的配置方式,分别是:
在application.yml中进行配置
spring:
security:
user:
# 设置用户名
name: qin
# 设置密码
password: 123123
当什么都没有配置的时候,账号和密码是由SpringSecurity自动生成的。而在实际的项目开发中,账号和密码都是从数据库中查询出来的。所以,我们可以通过自定义逻辑控制认证。
如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。
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.formLogin() //表单登陆
.and()
.authorizeRequests() //认证配置
.anyRequest() //任何请求
.authenticated(); //都需要身份验证
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
定义MyUserDetailsService类,通过@Service注解,按照名称将SecurityConfig类中的UserDetailsService对象的名称注入。
package com.example.demo01.configuration;
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 auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);
}
}
实际开发中,我们一般会通过数据库来认证用户信息,大概实现思路是在自定义UserDetailsService类中的loadUserByUsername方法中根据用户名去数据库中查询用户信息,如果满足要求,则用户通过认证,如果不满足要求,则抛出一个UsernameNotFoundException异常,该异常时SpringSecurity内部定义的用于抛出用户不存在的异常。
Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝
在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.formLogin() //表单登陆
.and()
.authorizeRequests() //认证配置
//设置哪些路径可以访问,,不需要认证
.antMatchers("/", "/test/hello").permitAll()
//当前登录的用户,只有具有admin权限,才可以访问这个路径
// .antMatchers("/test/index").hasAuthority("admin")
//当前登录的用户,只有具有admin和manager任一权限,就可以访问这个路径
.antMatchers("/test/index").hasAnyAuthority("admin","manager")
.anyRequest() //任何请求
.authenticated(); //都需要身份验证
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
在MyUserDetailsService类中添加授权的逻辑(用户被授予的权限)
package com.example.demo01.configuration;
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 {
//设置的authorityString必须和SecurityConfig中设置的hasAuthority字符串一致
//当前用户具有admin的权限
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);
}
}
Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝
在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.formLogin() //表单登陆
.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(); //都需要身份验证
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
在MyUserDetailsService类中添加授权的逻辑(用户被授予的角色)
package com.example.demo01.configuration;
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 {
//设置的authorityString必须和SecurityConfig中设置的hasAuthority字符串一致
//当前用户具有admin的权限
//当前用户具有sale的角色,角色必须加ROLE_的前缀
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);
}
}
在resources/static目录下定义一个访问被拒的页面unauth.html
Title
访问被拒,您没有权限浏览该页面
在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.exceptionHandling().accessDeniedPage("/unauth.html");
...
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
效果:
在resources/static目录下定义一个访问被拒的页面login.html
Title
在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.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //表单登陆
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/test/index").permitAll() //登录成功之后跳转路径
.and()
...
}
@Bean
BCryptPasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
在resources/static目录下定义一个访问被拒的页面success.html,当用户登陆成功后进入到这个页面,该页面提供一个用户注销的按钮。
Title
登录成功
退出登录
在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();
}
}
测试时,直接浏览器访问登陆页面http://localhost:8080/login.html,输入正确的用户名和密码进行登陆,登录成功后跳转到success.html页面,此时打开新的页面输入http://localhost:8080/test/index发现是可以正常访问内容的,但是在success.html点击退出登录,此时再次访问刚才test/index接口,系统则提示重新登陆。
Spring Security本质是一个过滤器链,有三个重要过滤器,分别是:
--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);
}
}
--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);
}
}
--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);
}
}
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,是对bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。