SpringSecurity
- 一、SpringSecurity基本介绍
- 二、SpringBoot整合SpringSecurity
- 2.1 添加SpringSecurity的依赖
- 2.2 启动访问
- 三、使用自定义登录界面
- 3.1 创建Security配置类
- 3.2 创建登录页面login.html
- 3.3 启动访问
- 四、使用数据库认证
- 4.1 创建一个Service继承UserDetailService
- 4.2 根据username获取用户信息
- 4.3 修改 SecurityConfig
- 4.4 重启访问
- 五、密码加密认证
- 5.1 修改UserDetailsService实现类
- 5.2 修改 SecurityConfig
- 5.3 重启访问
- 六、授权
- 6.1 开启全局方法配置
- 6.2 修改 UserDetailsService
- 6.3 接口添加声明书注解
- 6.3.1 启用prePostEnabled
- 6.3.1.1 PreAuthorize、PostAuthorize
- 6.3.1.2 PreFilter、PostFilter
- 6.3.1.3 prePostEnabled的内置表达式
- 6.3.2 启用securedEnabled
- 6.3.3 启用jsr250Enabled
- 6.4 重启访问
- 七、账号验证异常处理
一、SpringSecurity基本介绍
Spring Security 是一个基于 Spring 的企业应用系统提供声明式的安全访问控制接口方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的Bean,充分利用了Spring核心组件 IoC,DI 和 AOP ,为应用系统提供声明书的安全访问控制功能,减少了企业系统安全控制大量重复代码的编写。
二、SpringBoot整合SpringSecurity
2.1 添加SpringSecurity的依赖
新建一个SpringBoot项目,添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--SpringSecurity的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 启动访问
重启访问即可跳转到对应的登录界面
注意:系统启动的时候会自动创建一个账号是 user,密码随机的用户(密码显示在控制台)
使用该用户即可登录成功
三、使用自定义登录界面
3.1 创建Security配置类
SecurityConfig
@Configuration
// 开启Security配置
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证配置
auth.inMemoryAuthentication()
.withUser("test01") // 登录账号
.password("{noop}123") // 登录密码,其中{noop}表示不对密码进行加密验证
.roles("USER_ROLE"); // 账户角色
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Http请求配置
http.formLogin()
.loginPage("/login.html") // 指定自定义的登录界面
.loginProcessingUrl("/login.do") // 注意:必须和登录表单的 action一致!!!
.and()
.authorizeRequests() // 定义哪些资源被保护
.antMatchers("/login.html")
.permitAll() // login.html可以匿名访问
.anyRequest()
.authenticated(); // 除登录页面其他都需要认证
http.csrf().disable(); // 禁用跨域攻击
}
}
3.2 创建登录页面login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title></head>
<body>
<h2>自定义登录页面</h2>
<form action="/login.do" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
注意:登录页面需放入resources的status目标下,如图:
3.3 启动访问
重启访问即可跳转到自定义的登录界面
四、使用数据库认证
4.1 创建一个Service继承UserDetailService
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 重写 UserDetailsService 中的 loadUserByUsername 方法
return null;
}
}
4.2 根据username获取用户信息
根据username获取用户基本信息,角色信息等
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 重写 UserDetailsService 中的 loadUserByUsername 方法
// 用户不存在直接返回 null:表示无权访问
/**
* 模拟数据库操作 根据username获取用户基本信息,角色信息等
*/
String password = "123";
// 用户角色
List<SimpleGrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority("USER_ROLE"));
// {noop}表示不对密码进行加密验证
UserDetails userDetails = new User(username, "{noop}" + password, list);
return userDetails;
}
}
4.3 修改 SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证配置
/*auth.inMemoryAuthentication()
.withUser("test01")
.password("{noop}123")
.roles("USER_ROLE");*/
// 添加自定义登录验证
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Http请求配置
http.formLogin()
.loginPage("/login.html") // 指定自定义的登录界面
.loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
.and()
.authorizeRequests() // 定义哪些资源被保护
.antMatchers("/login.html")
.permitAll() // login.html可以匿名访问
.anyRequest()
.authenticated(); // 除登录页面其他都需要认证
http.csrf().disable(); // 禁用跨域攻击
}
}
4.4 重启访问
五、密码加密认证
5.1 修改UserDetailsService实现类
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 重写 UserDetailsService 中的 loadUserByUsername 方法
// 用户不存在直接返回 null:表示无权访问
/**
* 模拟数据库操作 根据username获取用户基本信息,角色信息等
*/
// 加密后的密码
String password = "$2a$10$XN/6HZGG1M1TfOr4Hj4hHeSh48BLHQom7g8aveHEm8MvCAyW6ajN2";
// 用户角色
List<SimpleGrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority("USER_ROLE"));
// 去除 {noop},表示需要对密码进行加密验证
// UserDetails userDetails = new User(username, "{noop}" + password, list);
UserDetails userDetails = new User(username, password, list);
return userDetails;
}
// 密码生成器
public static void main(String[] args) {
// SHA-256 + 随机salt
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
System.out.println(password);
}
}
5.2 修改 SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证配置
/*auth.inMemoryAuthentication()
.withUser("test01")
.password("{noop}123")
.roles("USER_ROLE");*/
// 添加自定义登录验证
auth.userDetailsService(userService)
// 指定密码 解密器
.passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Http请求配置
http.formLogin()
.loginPage("/login.html") // 指定自定义的登录界面
.loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
.and()
.authorizeRequests() // 定义哪些资源被保护
.antMatchers("/login.html")
.permitAll() // login.html可以匿名访问
.anyRequest()
.authenticated(); // 除登录页面其他都需要认证
http.csrf().disable(); // 禁用跨域攻击
}
}
5.3 重启访问
六、授权
6.1 开启全局方法配置
在 SecurityConfig
类上添加 @EnableGlobalMethodSecurity
注解。
@Configuration
@EnableWebSecurity
// 开启全局方法配置:这个注解必须开启,否则@PreAuthorize等注解不生效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
其中注解 @EnableGlobalMethodSecurity
有几个方法:
- prePostEnabled: 确定前置注解[@PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter] 是否启用
- securedEnabled: 确定安全注解 [@Secured] 是否启用
- jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed…]是否启用
在同一个应用程序中,可以启用多个类型的注解,但是只应该设置一个注解对于行为类的接口或者类。
6.2 修改 UserDetailsService
为用户添加角色
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 重写 UserDetailsService 中的 loadUserByUsername 方法
// 用户不存在直接返回 null:表示无权访问
/**
* 模拟数据库操作 根据username获取用户基本信息,角色信息等
*/
// 加密后的密码
String password = "$2a$10$XN/6HZGG1M1TfOr4Hj4hHeSh48BLHQom7g8aveHEm8MvCAyW6ajN2";
// 用户角色
List<SimpleGrantedAuthority> list = new ArrayList<>();
if ("test01".equals(username)) {
// 如果用户为 test01 为其添加 USER_ROLE 角色
list.add(new SimpleGrantedAuthority("USER_ROLE"));
}
// 去除 {noop},表示需要对密码进行加密验证
// UserDetails userDetails = new User(username, "{noop}" + password, list);
UserDetails userDetails = new User(username, password, list);
return userDetails;
}
public static void main(String[] args) {
// SHA-256 + 随机salt
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
System.out.println(password);
}
}
6.3 接口添加声明书注解
6.3.1 启用prePostEnabled
Spring Security 中定义了四个支持使用表达式的注解:
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。
注意:要使它们的定义能够对我们的方法的调用产生影响,在 Spring 中,我们需要设置
global-method-security元素的pre-post-annotations=”enabled”,默认为disabled。
<security:global-method-security pre-post-annotations="disabled"/>
在springboot中,需要添加 @EnableGlobalMethodSecurity(prePostEnabled = true)
注解。
6.3.1.1 PreAuthorize、PostAuthorize
-
@PreAuthorize
是在方法执行前进行权限认证 -
@PostAuthorize
是方法执行后在返回前进行权限认证
示例语法:
// 拥有 ROLE_USER 才能访问
@PreAuthorize("hasRole('ROLE_USER')")
// 拥有 ROLE_USER 或 ROLE_ADMIN 才能访问
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
// 拥有 sys:dept:delete 中的一种角色才能访问
@PreAuthorize("hasAuthority('sys:dept:delete')")
// 限制只能 Id小于10的用户才能访问
@PreAuthorize("#id<10")
// 限制只有username参数为自己用户名才能访问
@PreAuthorize("principal.username.equals(#username)")
public User find(String username) {
}
// 限制只有用户名称为abc的用户才能访问
@PreAuthorize("#user.name.equals('abc')")
public void add(User user) {
}
6.3.1.2 PreFilter、PostFilter
- @PreFilter 是在执行方法之前过滤集合或数组
- @PostFilter 是在执行方法后过滤返回的集合或数组
@PreFilter 和 @PostFilter 可以对集合类型的参数或返回值进行过滤。使用 @PreFilter 和 @PostFilter 时,Spring Security 将移除使对应表达式的结果为 false 的元素。
示例语法:
// 移除返回结果中id不为偶数的user
@PostFilter("filterObject.id%2==0")
public List<User> findAll() {
}
// 移除ids集合中不为偶数的对象
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
}
注意:filterObject 是使用 @PreFilter 和 @PostFilter 时的一个内置表达式,表示集合中的当前对象。当 @PreFilter 标注的方法拥有多个集合类型的参数时,需要通过 @PreFilter 的filterTarget 属性指定当前 @PreFilter 是针对哪个参数进行过滤的。
6.3.1.3 prePostEnabled的内置表达式
表达式 | 描述 |
hasRole([role]) | 当前用户是否拥有指定角色。 |
hasAnyRole([role1,role2]) | 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。 |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
Principle | 代表当前用户的principle对象 |
authentication | 直接从SecurityContext获取的当前Authentication对象 |
permitAll | 总是返回true,表示允许所有的 |
denyAll | 总是返回false,表示拒绝所有的 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isRememberMe() | 表示当前用户是否是通过Remember-Me自动登录的 |
isAuthenticated() | 表示当前用户是否已经登录认证成功了。 |
isFullyAuthenticated() | 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。 |
6.3.2 启用securedEnabled
在 Springboot 中,需要添加 @EnableGlobalMethodSecurity(securedEnabled = true))
注解。
在需要控制访问的接口上,添加 @Secured
注解。
@RestController
public class TestController {
/**
* 只有拥有 USER_ROLE 角色的用户才能访问
* @return
*/
@Secured({"USER_ROLE"})
@GetMapping("/test")
public String test() {
return "success";
}
/**
* 不添加任何授权注解:表示所有用户均可访问
* @return
*/
@GetMapping("/test1")
public String test1() {
return "success";
}
}
@Secured
注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured
,并且只有那些角色/权限的用户才可以调用该方法。
@Secured
缺点(限制)就是不支持 Spring EL 表达式。不够灵活。并且指定的角色必须以 ROLE_ 开头,不可省略。
6.3.3 启用jsr250Enabled
在 Springboot 中,需要添加 @EnableGlobalMethodSecurity(jsr250Enabled = true)
注解。
jsr250Enabled 注解比较简单,只有
- @DenyAll: 拒绝所有访问
- @RolesAllowed({“USER”, “ADMIN”}): 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
- @PermitAll: 允许所有访问
6.4 重启访问
使用不同账号登录访问,只有用户名为 test01 的账号存在 /test
接口访问权限。
七、账号验证异常处理
在实际开发中,对于登录异常,往往需要给出友好的错误提示,如:账户被锁定,密码过期,用户被禁用等,SpringSecurity 给我们一共了友好的身份校验失败异常处理器,AuthenticationFailureHandler,如下:
@Configuration
@EnableWebSecurity
// 开启全局方法配置:这个注解必须开启,否则@PreAuthorize等注解不生效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证配置
/*auth.inMemoryAuthentication()
.withUser("test01")
.password("{noop}123")
.roles("USER_ROLE");*/
// 添加自定义登录验证
auth.userDetailsService(userService)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Http请求配置
http.formLogin()
.loginPage("/login.html") // 指定自定义的登录界面
.loginProcessingUrl("/login.do") // 必须和登录表单的 action一致
// 身份验证失败 异常处理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(403);
Map<String, Object> errorMsgMap = new HashMap<>(2);
errorMsgMap.put("code", 403);
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
errorMsgMap.put("message", "账户名或者密码输入错误!");
} else if (e instanceof LockedException) {
errorMsgMap.put("message", "账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
errorMsgMap.put("message", "密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
errorMsgMap.put("message", "账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
errorMsgMap.put("message", "账户被禁用,请联系管理员!");
} else {
errorMsgMap.put("message", "登录失败!");
}
httpServletResponse.getWriter().write(errorMsgMap.toString());
}
})
.and()
.authorizeRequests() // 定义哪些资源被保护
.antMatchers("/login.html")
.permitAll() // login.html可以匿名访问
.anyRequest()
.authenticated(); // 除登录页面其他都需要认证
http.csrf().disable(); // 禁用跨域攻击
}
}