一、概述
Spring Security 是一个企业级安全框架,由spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在springboot技术推出以后,配置方面做了很大的简化
二、基本架构
Spring Security 在企业中实现认证和授权业务时,底层构建了大量的过滤器
其中:
绿色部分为认证过滤器,需要我们自己配置,也可以配置多个认证过滤器.也可以使用Spring Security提供的默认认证过滤器.黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.
三、调用流程
在过滤器中依据用户名和密码构建了一个UsernamePasswordAuthenticationToken(Authentication的一个实现)对象,其实就是一个Authentication的实现,他封装了我们需要的认证信息。之后会调用AuthenticationManager.。这个类其实并不会去验证我们的信息,信息验证的逻辑都是在AuthenticationProvider里面,而Manager的作用则是去管理Provider,管理的方式是通过for循环去遍历(因为不同的登录逻辑是不一样的,比如表单登录、第三方登录。
AuthenticationProvider去调用UserDetailsService拿到用户信息,然后做一些检查。最后把用户信息拼装到一个已经认证了的Authentication里面
四、入门案例
创建springboot工程,添加springboot基础相关依赖,这里不介绍啦
pom文件添加spring security相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动后控制台生成一个密码
浏览器访问 http://localhost:8080/login
用户名 :user(系统默认),密码:服务启动时,控制台默认输出的密码:,点击Sign in进行登录,登录成功默认会出现,如下界面:
自定义登录成功页面
在项目的resources目录下创建static目录,并在此目录创建一个index.html文件,
启动服务,再次进行登录访问测试,登录成功以后系统默认会跳转到index.html页面,例如
application.yml文件配置登录用户名和密码
spring:
security:
user:
name: jack
password: 123456
重启项目,访问登录页面 用户名/密码分别为上面yml文件配置的 jack/123456
但上面配置过于简单,我们需要对密码进行加密处理
编写测试类
public static void main(String[] args) {
BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
String password="123456";//明文
String newPwd=encoder.encode(password);
System.out.println(newPwd);//$2a$10$rl86aosqAfoLcmuEo9TfCO4QE532UmDeeVrd0.lflKwfvRkKd/.gi
}
application.yml 配置加密的密码以及加密算法
spring:
security:
user:
name: jack #这种写法,密码太简单了
#{bcrypt}指定了密码加密时使用的算法
password: '{bcrypt}$2a$10$rl86aosqAfoLcmuEo9TfCO4QE532UmDeeVrd0.lflKwfvRkKd/.gi'
说明:Bcrypt,底层基于随机盐方式对密码进行hash不可逆加密,更加安全,缺陷是慢
重启服务器,访问登录页面,用户名/密码: jack/123456
五、SpringSecurity认证逻辑实现
SpringSecurity支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,那就是系统上线后,用户信息的变更比较麻烦。因此SpringSecurity还支持通过实现UserDetailsService接口的方式来提供用户认证授权信息,其应用过程如下:
1 数据库表设计
数据库查询用户权限sql基础示例
SELECT GROUP_CONCAT(t.permission) from (
SELECT DISTINCT m.permission from sys_user u
LEFT JOIN sys_user_role ur on u.id= ur.user_id
LEFT JOIN sys_role_menu rm on ur.role_id=rm.role_id
LEFT JOIN sys_menu m on rm.menu_id=m.id WHERE u.id=1
)t
2 自定义登录逻辑
定义UserDetailService接口实现类,自定义登陆逻辑,UserDetailService为SpringSecurity官方提供的登录逻辑处理对象,我们自己可以实现此接口,然后在对应的方法中进行登录逻辑的编写即可.
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysAuthorityMapper sysAuthorityMapper;
/**
* 当我们执行登录操作时,底层会通过过滤器等对象调用这个方法
* @param username 这个参数为登录页面输入的用户名
* @return 从数据库基于用户名查询到的用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库查询到用户的信息,包含用户名,密码等
SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<>(new SysUser().setUsername(username)));
//假如分配权限的方式是角色,编写字符串时用"ROLE_"做前缀,示例"ROLE_admin,ROLE_normal,sys:res:retrieve,sys:res:create"
String dataAuthorities = sysAuthorityMapper.selectGrantedAuthorities(sysUser.getId());
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(dataAuthorities);
//这个user是SpringSecurity提供的UserDetails接口的实现,用于封装用户信息
// 后续我们也可以基于需要自己构建UserDetails接口的实现
return new User(username, sysUser.getPassword(), grantedAuthorities);
}
说明,这里的User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.
启动服务进行登陆,访问测试。
3 自定义登陆页面
在resources目录下static目录下新建login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="cc1471a5-3246-43ff-bef7-31d714273899" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body>
</html>
注意:请求的url暂时为”/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePasswordAuthenticationFilter中进行了定义。
添加安全配置类SpringSecurityConfig,让其实现接口,并重写相关config方法,进行登陆设计,代码如下:
/**
* 由@Configuration注解描述的类为spring中的配置类,配置类会在spring启动时优先加载,在配置类中通常会对第三方资源进行配置
*/
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 对http请求的安全控制进行配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨域攻击
//自定义登陆表单
http.formLogin()
.loginPage("/login.html")//设置登录页面
.loginProcessingUrl("/login")//设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
.usernameParameter("username")//设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
.passwordParameter("password") //请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
//.successForwardUrl("/index");//请求转发controller中的一个方法
.defaultSuccessUrl("/index.html")//设置登录成功跳转页面
.failureUrl("/login.html?error")//登陆失败访问的页面(默认为/login.html?error)
;
//设计登出信息
http.logout()
.logoutUrl("/logout") //登出路径
.logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面
//认证设计
http.authorizeRequests()
//设置要放行的资源
//“*”用于匹配0个或多个字符 “**”用于匹配0个或多个目录及字符
.antMatchers("/login.html", "/images/**").permitAll() //设置上述所有路径不需要登录就能访问(放行)
//设置需要认证的请求(除了上面的要放行,其它都要进行认证)
.anyRequest().authenticated()
;
}
/**
* 定义密码加密对象
*
* @Bean注解通常会在@Configuration注解描述的类中描述方法,用于告诉spring框架这个方法的返回值会交给spring管理 并且spring管理的对象起个默认的名字,这个名字与方法名相同,当然也可以通过@Bean注解起名字
*/
@Bean//对象名默认会方法名passwordEncoder
//@Bean("bCryptPasswordEncoder") 对象名为bCryptPasswordEncoder
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4 登陆成功和失败处理器
现在的很多系统都采用的是前后端分离设计,我们登陆成功以后可能会跳转到前端系统的某个地址,或者返回一个json数据,我们可以自己定义登录成功的处理操作
4.1 成功执行重定向的处理器
SpringSecurityConfig 类添加登录成功可以直接执行重定向的处理器代码
/**
* 登录成功可以直接执行重定向的处理器
* @param redirectUrl 登录成功要跳转的url
*/
public AuthenticationSuccessHandler redirectAuthenticationSuccessHandler(String redirectUrl) {
return (req, res, authentication) -> res.sendRedirect(redirectUrl);
}
4.2 成功返回JSON数据的处理器
编写工具类,定义统一返回数据格式
public class WebUtils {
/**
* 将数据以json格式写到客户端
* @param response 响应对象,负责向客户端输出数据的对象
*/
public static void writeJsonToClient(HttpServletResponse response,String[] key,Object[] value) throws IOException {
//1、设置响应数据的编码
response.setCharacterEncoding("utf-8");
//2.告诉浏览器响应数据的内容类型以及编码
response.setContentType("application/json;charset=utf-8");
//3.将数据转换为json格式字符串
String jsonStr = new ObjectMapper().writeValueAsString(getDataMap(key,value));
//4.获取输出流对象将json数据写到客户端
//4.1获取输出流对象
PrintWriter out = response.getWriter();
//4.2通过输出流向网络客户端写数据
out.println(jsonStr);
out.flush();
out.close();
}
/**
* 根据传入的key,value封装map数据
*/
public static Map<String,Object> getDataMap(String[] key,Object[] value){
Map<String,Object> map =new HashMap<>();
for(int i=0;i<key.length;i++){
map.put(key[i],value[i]);
}
return map;
}
}
SpringSecurityConfig 类添加登录成功直接返回JSON数据的处理器代码
/**
* 登录成功直接返回JSON数据的处理器
*/
public AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_OK, "login ok"});
}
4.3 失败重定向到页面的处理器
SpringSecurityConfig 类添加登陆失败重定向到页面的处理器代码
/**
* 登陆失败重定向到页面
*/
public AuthenticationFailureHandler redirectAuthenticationFailureHandler(String redirectUrl) {
return (req, res, authenticationException) -> res.sendRedirect(redirectUrl);
}
4.4 失败返回json数据的处理器
SpringSecurityConfig 类添加登陆失败返回json数据的处理器代码
/**
* 登录失败返回json数据
*/
public AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "username or password error"});
}
5 设置登录成功失败处理器
SpringSecurityConfig类中configure方法添加成功失败处理器代码
SpringSecurityConfig类总代码
package com.zhanlijuan.config;
import com.zhanlijuan.util.WebUtils;
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.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
/**
* 由@Configuration注解描述的类为spring中的配置类,配置类会在spring启动时优先加载,在配置类中通常会对第三方资源进行配置
*/
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 对http请求的安全控制进行配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨域攻击
//自定义登陆表单
http.formLogin()
.loginPage("/login.html")//设置登录页面
.loginProcessingUrl("/login")//设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
.usernameParameter("username")//设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
.passwordParameter("password") //请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
//.successForwardUrl("/index");//请求转发controller中的一个方法
.defaultSuccessUrl("/index.html")//设置登录成功跳转页面
.failureUrl("/login.html?error")//登陆失败访问的页面(默认为/login.html?error)
//.successHandler(redirectAuthenticationSuccessHandler("https://www.baidu.com")) 登录成功重定向到https://www.baidu.com"
.successHandler(jsonAuthenticationSuccessHandler())//登录成功返回json数据
//.failureHandler(redirectAuthenticationFailureHandler("https://www.jd.com/"))//登录失败跳转到https://www.jd.com/
.failureHandler(jsonAuthenticationFailureHandler())//失败返回json数据
;
//设计登出信息
http.logout()
.logoutUrl("/logout") //登出路径
.logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面
//认证设计
http.authorizeRequests()
//设置要放行的资源
//“*”用于匹配0个或多个字符 “**”用于匹配0个或多个目录及字符
.antMatchers("/login.html", "/images/**").permitAll() //设置上述所有路径不需要登录就能访问(放行)
//设置需要认证的请求(除了上面的要放行,其它都要进行认证)
.anyRequest().authenticated()
;
}
/**
* 登录成功可以直接执行重定向的处理器
*
* @param redirectUrl 登录成功要跳转的url
*/
public AuthenticationSuccessHandler redirectAuthenticationSuccessHandler(String redirectUrl) {
return (req, res, authentication) -> res.sendRedirect(redirectUrl);
}
/**
* 登录成功直接返回JSON数据的处理器
*/
public AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_OK, "login ok"});
}
/**
* 登陆失败重定向到页面
*/
public AuthenticationFailureHandler redirectAuthenticationFailureHandler(String redirectUrl) {
return (req, res, authenticationException) -> res.sendRedirect(redirectUrl);
}
/**
* 登录失败返回json数据
*/
public AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "username or password error"});
}
/**
* 定义SpringSecurity密码加密对象
*
* @Bean注解通常会在@Configuration注解描述的类中描述方法,用于告诉spring框架这个方法的返回值会交给spring管理 并且spring管理的对象起个默认的名字,这个名字与方法名相同,当然也可以通过@Bean注解起名字
*/
@Bean//对象名默认会方法名passwordEncoder
//@Bean("bCryptPasswordEncoder") 对象名为bCryptPasswordEncoder
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
启动服务进行访问测试(分别用正确和错误的账号进行测试)。
六、SpringSecurity授权逻辑实现
1 添加启用全局方法访问控制注解
SpringSecurityConfig 配置类添加全局方法访问控制注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
说明:注解由SpringSecurity提供,用于描述权限配置类,告诉系统底层在启动时,进行访问权限的初始化配置其中prePostEnabled= true表示启动权限管理功能
2 定义资源Controller
定义一个ResourceController类,作为资源访问对象,
@RestController
public class ResourceController {
@PreAuthorize("hasAuthority('sys:user:update')")
@GetMapping("/doCreate")
public String doCreate(){
return "create resource ok";
}
@PreAuthorize("hasAuthority('sys:user:delete')")
@GetMapping("/doDelete")
public String doDelete(){
return "delete resource ok";
}
}
其中,@PreAuthorize注解描述方法时,用于告诉系统访问此方法时需要进行权限检测。需要具备指定权限才可以访问。例如:
@PreAuthorize(“hasAuthority('sys:user:delete”) 需要具备sys:user:delete权限
@PreAuthorize(“hasRole(‘admin’)”) 需要具备admin角色
使用不同用户进行登陆,然后执行资源访问,假如没有权限,则会看到响应状态吗403,如图所示:
七、SpringSecurity认证和授权异常处理
1 异常类型
对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:
- AuthenticationException 用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401
- AccessDeniedException 用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403
2 异常处理规范
SpringSecurity框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务需求时,此时我们就要自己定义异常处理逻辑,编写逻辑时需要遵循如下规范:
1)AuthenticationEntryPoint:统一处理 AuthenticationException 异常
2)AccessDeniedHandler:统一处理 AccessDeniedException 异常.
3 自定义异常处理逻辑
3.1 处理没有认证的访问异常
SpringSecurityConfig 添加代码
public AuthenticationEntryPoint authenticationEntryPoint() {
return (req, res, authenticationException) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_UNAUTHORIZED, "请先登录"});
}
3.2 处理没有权限时抛出的异常
SpringSecurityConfig 添加代码
public AccessDeniedHandler accessDeniedHandler(){
return (req, res, accessDeniedException ) -> WebUtils.writeJsonToClient(res, new String[]{"state","msg"}, new Object[]{HttpServletResponse.SC_FORBIDDEN,"没有访问权限,请联系管理员"});
}
3.3 配置异常处理对象
在配置类SecurityConfig中添加自定义异常处理对象,代码如下
//需要认证与拒绝访问的异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())//没有认证时执行
.accessDeniedHandler(accessDeniedHandler());//没有授权是执行DefaultAccessDeniedExceptionHandler
SecurityConfig类全部代码
/**
* 由@Configuration注解描述的类为spring中的配置类,配置类会在spring启动时优先加载,在配置类中通常会对第三方资源进行配置
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 对http请求的安全控制进行配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();//关闭跨域攻击
//自定义登陆表单
http.formLogin()
.loginPage("/login.html")//设置登录页面
.loginProcessingUrl("/login")//设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
.usernameParameter("username")//设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
.passwordParameter("password") //请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
//.successForwardUrl("/index");//请求转发controller中的一个方法
.defaultSuccessUrl("/index.html")//设置登录成功跳转页面
.failureUrl("/login.html?error")//登陆失败访问的页面(默认为/login.html?error)
//.successHandler(redirectAuthenticationSuccessHandler("https://www.baidu.com")) 登录成功重定向到https://www.baidu.com"
.successHandler(jsonAuthenticationSuccessHandler())//登录成功返回json数据
//.failureHandler(redirectAuthenticationFailureHandler("https://www.jd.com/"))//登录失败跳转到https://www.jd.com/
.failureHandler(jsonAuthenticationFailureHandler())//失败返回json数据
;
//需要认证与拒绝访问的异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())//没有认证时执行
.accessDeniedHandler(accessDeniedHandler());//没有授权是执行DefaultAccessDeniedExceptionHandler
//设计登出信息
http.logout()
.logoutUrl("/logout") //登出路径
.logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面
//认证设计
http.authorizeRequests()
//设置要放行的资源
//“*”用于匹配0个或多个字符 “**”用于匹配0个或多个目录及字符
.antMatchers("/login.html", "/images/**").permitAll() //设置上述所有路径不需要登录就能访问(放行)
//设置需要认证的请求(除了上面的要放行,其它都要进行认证)
.anyRequest().authenticated()
;
}
/**
* 没有认证的访问异常逻辑
*/
public AuthenticationEntryPoint authenticationEntryPoint() {
return (req, res, authenticationException) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_UNAUTHORIZED, "请先登录"});
}
/**
* 处理没有权限时抛出的异常
*/
public AccessDeniedHandler accessDeniedHandler() {
return (req, res, accessDeniedException) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_FORBIDDEN, "没有访问权限,请联系管理员"});
}
/**
* 登录成功可以直接执行重定向的处理器
*
* @param redirectUrl 登录成功要跳转的url
*/
public AuthenticationSuccessHandler redirectAuthenticationSuccessHandler(String redirectUrl) {
return (req, res, authentication) -> res.sendRedirect(redirectUrl);
}
/**
* 登录成功直接返回JSON数据的处理器
*/
public AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_OK, "login ok"});
}
/**
* 登陆失败重定向到页面
*/
public AuthenticationFailureHandler redirectAuthenticationFailureHandler(String redirectUrl) {
return (req, res, authenticationException) -> res.sendRedirect(redirectUrl);
}
/**
* 登录失败返回json数据
*/
public AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
return (req, res, authentication) -> WebUtils.writeJsonToClient(res, new String[]{"state", "msg"}, new Object[]{HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "username or password error"});
}
/**
* 定义SpringSecurity密码加密对象
*
* @Bean注解通常会在@Configuration注解描述的类中描述方法,用于告诉spring框架这个方法的返回值会交给spring管理 并且spring管理的对象起个默认的名字,这个名字与方法名相同,当然也可以通过@Bean注解起名字
*/
@Bean//对象名默认会方法名passwordEncoder
//@Bean("bCryptPasswordEncoder") 对象名为bCryptPasswordEncoder
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
配置完成后,重启服务,测试