1. Spring Security 简介
1.1 背景分析
企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,有些只能登录以后才能访问,还有一些你登录成功以后,权限不够也不能访问.总之这些规则都是保护系统资源不被破坏的一种手段.几乎每个系统中都需要这样的措施对数据(资源)进行保护.我们通常会通过软件技术对这样的业务进行具体的设计和实现.早期没有统一的标准,每个系统都有自己独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共享做了具体的落地实现,例如SpringSecurity,Apache shiro诞生了.
1.2 Spring Security 概述
Spring Security 是一个企业级安全框架,有Spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在SpringBoot技术推出以后,配置方面做了很大的简化.市场上现在的分布式架构下的安全控制正在逐步的转向Spring Security.
1.3 Spring Security 基本架构
Spring Security 在企业中实现认证和授权业务时,低层构建了大量的过滤器.
其中:绿色部分为认证过滤器,需要我们自己配置,也可以配置多个认证过滤器.也可以使用Spring Security提供的默认认证过滤器.黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.
2. Spring Security 快速入门
2.1 创建工程
2.2 添加项目依赖
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
创建配置文件
在resources目录下创建application.yml文件,并指定服务端口
server:
port:8080
2.3 创建项目启动类并运行
第一步:检查控制台输出的密码
Using generated security password: 800993a7-c97b-4563-8b0e-ee42d292be2c
第二步:浏览器访问localhost:8080如下显示
用户名:user(系统默认)
密 码:800993a7-c97b-4563-8b0e-ee42d292be2c(控制台生成)
登陆后的效果:
404不是错,是因为没有默认登录页面
2.4 自定义登陆成功页面
在项目的resources目录下创建static目录,并在此目录创建一个index.html文件
重启服务再次登录访问测试,登录成功后系统默认跳转到index.html页面
2.5 配置登录密码
在application.yml文件中可配置用户名和密码
spring:
security:
user:
name: DJH
password: ghhyw
查看application.yml文件可以看到密码,可以采用密码加密算法生成密码,如MD5, BCrypt,如下:
其中,{bcrypt}是指定密码加密时使用的算法
3. Spring Security 认证逻辑实现
自定义登录逻辑
Spring Security支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,就是系统上线后,用户信息变更比较麻烦.因此Spring Security还支持通过实现UserDetailsService接口的方式来提供用户认证授权信息,其应用过程如下:
第一步:定义security配置类
/**
* 由@Configuration注解描述的类为spring中的配置类,配置类会在spring
* 工程启动时优先加载,在配置类中通常会对第三方资源进行初始配置.
*/
@Configuration
public class SecurityConfig {
/**
* 定义SpringSecurity密码加密对象
* @Bean 注解通常会在@Configuration注解描述的类中描述方法,
* 用于告诉spring框架这个方法的返回值会交给spring管理,并spring
* 管理的这个对象起个默认的名字,这个名字与方法名相同,当然也可以通过
* @Bean注解起名字
*/
@Bean("bcryptPasswordEncoder")//自定义名字,不写默认就是方法名
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
第二步:定义UserDetailService接口实现类,自定义登录逻辑,代码如下:
UserDetailService为Spring Security官方提供的登录逻辑处理对象,我们可以实现此接口,然后对应的方法中进行登录逻辑编写
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法.
*
* @param username 这个参数为页面输出的用户名
* @return 一般是从数据库基于用户名查询到的用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//1.基于用户名从数据库查询用户信息
if(!"jack".equals(username))
throw new UsernameNotFoundException("user no exists");
//2.将用户信息封装到UserDetails对象中并返回
//假设这个秘密是数据库查询的加密的
String encodePwd = passwordEncoder.encode("123456");
List<GrantedAuthority> grantedAuthorityList = AuthorityUtils
.commaSeparatedStringToAuthorityList(
"ROLE_jp, ROLE_tongpai, sys:res:retrieve, sys:res:create");
User user = new User(username, encodePwd, grantedAuthorityList);
return user;
}
}
说明,这里的User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.
第三步:启动服务进行登陆,访问测试。
自定义登录页面
第一步:在static目录下创建login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img src="../images/a.jpg">
<form action="/login" method="post">
<ul>
<li>username:</li>
<li><input type="text" name="username"></li>
<li>password:</li>
<li><input type="password" name="password"></li>
</ul>
<li><input type="submit" value="Sign in"></li>
</form>
</body>
</html>
注意:请求的url暂时为”/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePasswordAuthenticationFilter中进行了定义。
第二步:修改安全配置类,让其实现接口,并重写相关config方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//对http请求的安全控制进行配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//1.关闭跨域攻击
http.csrf().disable();
//2.配置登录url(登录表单使用那个页面)
http.formLogin()
//设置登陆页面
.loginPage("/login.html")
//设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
.loginProcessingUrl("/login")
//设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
//.usernameParameter("username")
//请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
//.passwordParameter("password")
//设置登陆成功跳转页面(默认为/index.html)
.defaultSuccessUrl("/index.html")
//.successForwardUrl("/index.html")//转发
// .successHandler(
// new RedirectAuthenticationSuccessHandler(
// "localhost:8080/index.html")
// );
//.failureHandler();
.failureUrl("/login.html?wocuole");
// 3.放行登录url(不许要认证就可以访问)
http.authorizeRequests()
.antMatchers("/login.html", "/images/**")//写放行的资源
.permitAll()//允许直接访问
.anyRequest().authenticated();//除了上面的资源必须认证才能访问
}
}
登陆成功和失败处理器
定义登录成功处理器
//实现AuthenticationSuccessHandler接口
public class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String redirectUrl;
public RedirectAuthenticationSuccessHandler(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication)
throws IOException, ServletException {
httpServletResponse.sendRedirect(redirectUrl);
}
}
放出静态资源
http.authorizeRequests()
.antMatchers("/login.html", "/images/**")//写放行的资源
.permitAll()//允许直接访问
.anyRequest().authenticated();//除了上面的资源必须认证才能访问
- *用于匹配0个或多个字符
- **用于匹配0个或多个目录及字符
登出设计及实现
在SecurityManager配置类中的configure(HttpSecurity http)方法中,添加登出配置,例如
http.logout() //开始设置登出信息
.logoutUrl("/logout") //登出路径
.logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面
4. Spring Security 授权逻辑实现
修改授权配置类
在权限配置类上添加启用全局方法访问控制注解
/**
* @EnableGlobalMethodSecurity 注解由SpringSecurity提供,
* 用于描述权限配置类,告诉系统底层在启动时,进行访问权限的初始化配置
* 1)Enable-启用
* 2)Global-全局
* 3)Method-方法
* 4)Security-安全
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
.....
}
定义Controller
@PreAuthorize注解描述方法时,用于告诉系统访问此方法时需要进行权限检测。需要具备指定权限才可以访问。例如:
@PreAuthorize(“hasAuthority('sys:res:delete”) 需要具备sys:res:delete权限
@PreAuthorize(“hasRole(‘admin’)”) 需要具备admin角色
package com.djh.jt.security.controller;
import com.sun.org.apache.regexp.internal.RE;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 可以将这里的Controller看成是系统内部的一个资源对象,
* 我们要求访问此对象中的方法时需要进行权限检查
*
*/
@RestController
public class ResourceController {
/**
* 添加操作
* @PreAuthorize 注解由SpringSecurity框架提供,用于描述方法,此注解描述
* 方法以后,再访问方法首先要进行权限检测
*/
//@PreAuthorize("hasRole('jt')")
@PreAuthorize("hasAuthority('sys:res:create')")
@RequestMapping("/doCreate")
public String doCreate() {
return "create resource (insert data) ok";
}
/**查询操作*/
@PreAuthorize("hasAuthority('sys:res:retrieve')")
@RequestMapping("/doRetrieve")
public String doRetrieve() {
return "retrieve resource (insert data) ok";
}
/**修改操作*/
@PreAuthorize("hasAuthority('sys:res:update')")
@RequestMapping("/doUpdate")
public String doUpdate() {
return "update resource (insert data) ok";
}
/**删除操作*/
@PreAuthorize("hasAuthority('sys:res:delete')")
@RequestMapping("/doDelete")
public String doDelete() {
return "delete resource (insert data) ok";
}
}
启动服务访问测试
使用不同用户进行登录,然后执行资源访问,假如没有权限,则会看到响应状态代码为403
5. SpringSecuilty认证和授权异常处理
异常类型
对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:
- AuthenticationException (用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401)
- AccessDeniedException (用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403)
SpringSecuilty框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务的需求时,此时我们就要自定义异常处理逻辑,编写逻辑时需要遵循如下规范:
- AuthenticationEntryPoint:统一处理 AuthenticationException 异常
- AccessDeniedHandler:统一处理 AccessDeniedException 异常
自定义异常处理对象
处理没有认证的访问异常
package com.djh.jt.security.config.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 假如用户没有认证,就去访问需要认证才可以访问的资源,
* 底层会抛出一个异常AuthenticationException,
* 系统对此异常的处理方式是跳转到登录页面,假如现在我们
* 不要跳转到登录页,而是需要返回一个json格式的字符串,
* 则需要自己定义AuthenticationEntryPoint接口的
* 实现类
*/
public class DefaulAuthenticationEntryPoint implements AuthenticationEntryPoint {
/**
* 当系统出现AuthenticationException异常时,
* 会自己动调用此方法(commence-开始)
* @param httpServletRequest 请求对象
* @param httpServletResponse 响应对象
* @param exception 异常
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException exception) throws IOException, ServletException {
//页面跳转,访问失败直接跳到某个地址
//方案一:重定向
//httpServletResponse.sendRedirect("www.baidu.com");
//方案二:假如访问被拒绝了向客户端响应一个json格式的字符串
//2.1 设置响应数据的编码
httpServletResponse.setCharacterEncoding("utf-8");
//2.2 告诉浏览器响应数据的内容类型以及编码
httpServletResponse.setContentType("application/json,charset=utf-8");
//2.3 获取输出流对象
PrintWriter out = httpServletResponse.getWriter();
//2.4 将数据输出到客户端
//2.4.1 封装数据
Map<String, Object> map = new HashMap<>();
map.put("state",
HttpServletResponse.SC_UNAUTHORIZED);
map.put("message",
"请先登录在访问");
// 2.4.2 将数据转换为json字符串,并输出数据
out.println(new ObjectMapper().writeValueAsString(map));
out.flush();
}
}
处理没有权限时抛出的异常
package com.djh.jt.security.config.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 默认访问拒绝异常处理器
* Default 默认
* Access 访问
* Denied 拒绝
* Exception 异常
* Handler 处理器
*/
public class DefaultAccessDeniedExceptionHandler implements AccessDeniedHandler {
/**
* 此方法用于处理AccessDeniedExceotion对象
* @param httpServletRequest 请求对象
* @param httpServletResponse 响应对象
* @param exception 访问被拒绝的异常
* @throws IOException
* @throws ServletException
*/
@Override
public void handle(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AccessDeniedException exception) throws IOException, ServletException {
//页面跳转,访问失败直接跳到某个地址
//方案一:重定向
//httpServletResponse.sendRedirect("www.baidu.com");
//方案二:假如访问被拒绝了向客户端响应一个json格式的字符串
//2.1 设置响应数据的编码
httpServletResponse.setCharacterEncoding("utf-8");
//2.2 告诉浏览器响应数据的内容类型以及编码
httpServletResponse.setContentType("application/json,charset=utf-8");
//2.3 获取输出流对象
PrintWriter out = httpServletResponse.getWriter();
//2.4 将数据输出到客户端
//2.4.1 封装数据
Map<String, Object> map = new HashMap<>();
map.put("state", HttpServletResponse.SC_FORBIDDEN);
map.put("message", "没有访问权限,请联系管理员");
// 2.4.2 将数据转换为json字符串,并输出数据
out.println(new ObjectMapper().writeValueAsString(map));
out.flush();
}
}
配置异常处理对象
在配置类SecurityConfig中添加自定义异常处理对象,代码如下:
http.exceptionHandling()
.authenticationEntryPoint(new DefaultAuthenticationEntryPoint())
.accessDeniedHandler(new DefaultAccessDeniedExceptionHandler());