目录
前言
正文
1.基本流程
2.基本用法
3.配置项
4.HttpSecurity 方式和内存认证方式
5.认证流程
6.基于数据库查询的登录验证
7.多种角色权限认证
8.自定义权限认证
总结
安全对于任何系统来说都是非常重要的,权限的分配和管理一直都是开发者需要特别重视的。一旦缺乏基本和有力的授权验证,一些别有用心之人就会利用这个漏洞对开发者的 Web 应用或者其他软件进行不法侵害。Spring Boot 技术中有许多优秀的安全框架和认证授权方案,本次将介绍比较流行的框架技术 Spring Security 及其实践应用。
Spring Security 是 Spring Boot 中一款功能强大基于 Spring 的企业级应用的提供安全访问权限的安装框架,在实际工程项目中也会经常用到。通过依赖注入的方式,可以使用 Spring Security 库提供声明式的安全访问控制功能。它和 Spring Boot 以及其他 Spring 模块紧密相连。
Spring Security 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示
通过 debug 查看当前的过滤器链。
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
System.out.println(run);//在此处打上断点
}
}
打开表达式计算面板
run.getBean(DefaultSecurityFilterChain.class)
计算求值:
安装,在 pom.xml 中导入依赖即可
org.springframework.boot
spring-boot-starter-security
在项目中编写一个测试的接口 /testHi,代码如下:
package org.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HiController {
@GetMapping("/hi")
public String hi(){
return "Hi";
}
}
编写一个类,更好的配置管理。
package org.example.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Security 配置类
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 认证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("freejava")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("VIP1");
}
/**
* 授权
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置匹配的资源白名单访问
http.authorizeRequests().antMatchers("/","/asserts/**",
"/pages/login.html","/userlogin")
.permitAll()
.antMatchers("/level1/**").hasRole("VIP1")
.antMatchers("/level2/**").hasRole("VIP2")
.antMatchers("/level3/**").hasRole("VIP3")
.anyRequest().authenticated();//剩余任何资源必须认证
//开启登录页
http.formLogin();
//开启自动注销
http.logout().logoutSuccessUrl("/login");//注销之后来到登录页
http.csrf().disable();
}
}
最后运行会出现如下界面:
如果想要修改默认的账号和密码,可以在 application.properties 文件中加入下面的配置项。
spring.security.user.name=freejava
spring.security.user.password=123456
spring.security.user.roles[]=admin
所谓内存认证就是自定义配置类,该配置类继承 WebSecurityConfigurerAdapter,需要实现一些自定义配置和方法,具体代码如下:
package org.example.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class RealSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
//NoOpPasswordEncoder 在高版本的Spring Boot 中已被弃用,不建议使用
return new BCryptPasswordEncoder();
}
@Autowired PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)//使用对应的加密
.withUser("admin")
.password(passwordEncoder.encode("123456"))//使用对应的解密
.roles("ADMIN","USER")
.and()
.withUser("freephp")
.password(passwordEncoder.encode("123456"))
.roles("USER");
}
}
上面这段代码中,inMemoryAuthentication 代表把这个配置保存在内存中,然后使用 withUser 方法增加授权账号,用 password 方法设置密码,用 roles 来设置账号所属的权限群组。
而 HttpSecurity 是另外一种认证方式,也是使用 configure 方法,具体代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN','USER')")
.anyRequest()
.authenticated()//任意登录的用户都可以访问
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
使用 andMatcher 设置需要被授权的 URL 路径,access 方法给予某些角色访问权限,代码如下:。
package org.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SwagController {
@GetMapping("/user/sayHi")
public String myUser(){
return "Hi,user";
}
@GetMapping("/admin/hello")
public String admin(){
return "admin page";
}
@GetMapping("/hello")
public String hello(){
return "hello,man";
}
}
运行项目后,访问 http://localhost:8080/admin/hello, 则会要求输入账号和密码,使用 admin 账号,密码输入 123456,即可进入后台 /admin/hello 页面,如图。
- Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口: 定义了认证Authentication的方法。
- UserDetailsService接口: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
- UserDetails接口: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中。
所以接下来如果要使用数据库做登录验证,只要把实现类 InMemoryUserDetailsManager 做一个替换,DaoAuthenticationProvider 调用自定义的实现类。让其不再使用内存做认证进入数据库查询,实现 UserDetailsService 接口即可。
之前都是使用内存来存储认证数据,其实可以考虑使用数据库进行持久化数据存储。这样更加方便进行账号管理,也更符合实际项目开发的需求。
创建一个 roles 库,然后创建用户表、角色权限表、用户和角色权限关系表。
create database r_security;
use r_security;
-- 用户表
CREATE TABLE `r_users`(
`id` int(11) unsigned NOT NULL AUTO_INCREMENT primary key COMMENT '主键',
`username` varchar(50) not null comment '账号名',
`password` varchar(300) not null comment '密码',
`status` tinyint(11) not null comment '账号状态,1:正常、2:被封',
`created` int (11) not null comment '创建时间,时间戳'
);
-- 角色权限表
create table `r_roles`(
`id` int(11) unsigned not null auto_increment comment '主键',
`name` varchar(50) not null comment '角色名',
`permission_path` varchar(500) not null comment '权限路径,如/admin/*',
primary key (id)
);
-- 用户角色权限关系表
create table `r_user_roles`(
`id` int(11) unsigned not null auto_increment comment '主键',
`user_id` int(11) unsigned not null comment '用户ID',
`role_id` int(11) unsigned not null comment '角色ID',
primary key (`id`),
key `user_id` (`user_id`),
key `role_id` (`role_id`),
constraint `role_id` foreign key (`role_id`) references `r_roles`(`id`) on
delete restrict ,
constraint `user_id` foreign key (`user_id`) references `r_users`(`id`) on
delete restrict
)
为了方便测试,先插入几条测试数据,r_users 的数据如图
R_roles的数据如图,插入三条数据,有三种角色,一是管理员角色,二是 root 权限,也就是超级管理员。三是 dba,数据管理员。这三种角色可以访问不同的 URL。
用户角色表中插入数据
为了生成上面 r_users 表中加密后的密码,攥写了使用 Bcrypt 加密的程序,代码如下:
import java.util.ArrayList;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BcryptTest {
public static void main(String[] args) {
ArrayList passwordArr = new ArrayList<>();
passwordArr.add("123456");
getUsersEncodePasswords(passwordArr);
}
private static void getUsersEncodePasswords(ArrayList passwordArr) {
for (String pass :passwordArr){
//密码加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String newPassword = passwordEncoder.encode(pass);
System.out.println(" 原始密码是:"+pass+",加密密码为:"+newPassword);
//对比这两个密码是不是同一个密码
boolean match = passwordEncoder.matches(pass,newPassword);
System.out.println("两个密码一致:"+match);
}
}
}
运行结果如图:
Bcrpt 加密算法非常安全,此算法自身实现了随机盐生成,很难被逆向破解。
创建一个新的项目,导入依赖,pom.xml代码如下:
4.0.0
org.example
Security2
1.0-SNAPSHOT
8
8
UTF-8
org.springframework.boot
spring-boot-starter-parent
2.3.5.RELEASE
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
2.3.5.RELEASE
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.1.1
com.alibaba
druid-spring-boot-starter
1.1.10
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
junit
junit
4.13.2
test
r_roles 表对应的部分实体类对象
package org.example.entity;
import lombok.Data;
@Data
public class Role {
// 主键 ID
private Integer id;
// 名称
private String name;
//权限路径
private String permission_path;
}
创建 r_users 对应的 POJO 对象 User,继承自 UserDetails 接口,代码如下:
package org.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
//主键 id
private Integer id;
//用户名
private String username;
//密码
private String password;
// 状态 1:正常,2:封禁
private int status;
// 创建时间
private int created;
private List roles;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
for(Role role : roles){
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return status != 2;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return status == 1;
}
}
Java 类 User 实现了 Spring Security 中的 UserDetails 接口,这是因为 Spring Security 需要具体的用户信息来进行认证和授权。通过实现 UserDetails 接口,User 类可以与 Spring Security 框架集成,允许 Spring Security 使用该类的实例来获取用户的安全相关信息。
- getAuthorities():返回授予用户的权限列表。在这个 User 类中,它遍历用户的角色列表 roles 并为每个角色创建一个 SimpleGrantedAuthority 对象,最终返回权限集合。
- getPassword():返回用户的密码。在这个 User 类中,这个方法返回 User 对象的 password 字段。
- getUsername():返回用户的用户名。在这个 User 类中,这个方法返回 User 对象的 username 字段。
- isAccountNonExpired():检查用户的账户是否已过期。在这个 User 类中,这个方法硬编码返回 true,这意味着账户被认为永远不会过期,在数据库设计中没有该字段,所以默认返回 true。在实际的应用程序中,你可能需要添加一些逻辑来判断账户是否真的过期了。
- isAccountNonLocked():检查用户是否未被锁定,返回 false 代表被锁定,true 没有被锁定。
- isCredentialsNonExpired():检查用户证书(密码)是否未过期。在这个 User 类中,这个方法返回 true,表明证书永远不会过期。但在实际应用中,你可能会根据业务逻辑添加相应的实现代码。
- isEnabled():检查用户是否被启用。在这个 User 类中,如果 status 字段的值为 1,代表用户状态正常,方法返回 true;否则返回 false。
Mapper 的编写如下:
package org.example.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.example.entity.Role;
import org.example.entity.User;
import java.util.List;
@Mapper
public interface Usermapper {
@Select("select * from r_roles as r join r_user_roles as ur on r.id = ur.user_id where ur.user_id = #{id}")
List getUserRoleByUserId(Integer id);
@Select("select * from r_users where username = #{username}")
User getUserByUsername(String username);
}
UserService 如下:
package org.example.service;
import org.example.entity.User;
import org.example.mapper.Usermapper;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private Usermapper usermapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = usermapper.getUserByUsername(username);
if (user == null){
throw new UsernameNotFoundException("该账户不存在");
}
//根据 user id 获取用户的角色信息
user.setRoles(usermapper.getUserRoleByUserId(user.getId()));
return user;
}
}
UserService
类实现了 Spring Security 中的UserDetailsService
接口,这个接口主要用于在认证过程中通过用户名查找用户及其权限。在loadUserByUsername
方法中,你使用了Usermapper
来获取具体的User
对象以及相关的角色信息。
为了测试方便编写 controller:
package org.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/dba/hi")
public String dba(){
return "Hi,dba page";
}
@GetMapping("/user/hi")
public String user(){
return "Hi,user";
}
@GetMapping("/admin/hi")
public String admin(){
return "Hi,admin";
}
@GetMapping("/test/Hi")
public String testHi(){
return "Hi,just for test";
}
}
最后对Spring Security 进行配置编写,代码如下:
package org.example.config;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Autowired
PasswordEncoder passwordEncoder;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/bda/**")
.access("hasAnyAuthority('dba','admin','root')")
//"hasAnyRole('dba','admin','root')") 会自动拼接头,Role_为头加上对应权限
.antMatchers("/admin/**").access(
"hasAnyAuthority('root','admin')")//不会拼接头,直接认证。
.and()
.formLogin()
.loginProcessingUrl("/login")
/*.and()
.anonymous()//未登录才可以访问 permitAll 所有都可以访问*/
.and().csrf().disable();
http
//访问被拒绝走以下
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
// 自定义响应逻辑
response.sendRedirect("/access-denied");
}
});
//如果需要跨域使用这里,并配置spring WebMVCconfig
http.cors();
}
}
成功
有时候一个账号的权限可能是多个,如 freejava 即使 admin 又是 dba。那么在配置中可以增加显示权限包含关系的代码,可以在 Spring Secuirty 中配置代码如下:
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_user ROLE_admin > ROLE_dba";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
该配置生效后,具有 ROLE_admin 的角色的用户可以访问所有资源,而 ROLE_dba 的角色用户可以访问自身权限的资源和 ROLE_user 的角色的用户资源。
如果我们不想使用默认提供的权限认证,或面对复杂的业务需要时,可能需要采取自定义认证方式实现权限认证。
自定义一个类
package org.example.expression;
import org.example.entity.Role;
import org.example.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component("ex")
public class U_Define_Expression {
public boolean hasAutority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User LoginUser = (User) authentication.getPrincipal();
List roles = LoginUser.getRoles();
List permissions = new ArrayList<>();
for(Role role :roles){
permissions.add(role.getName());
}
//判断用户权限集合中是否存在 authority
return permissions.contains(authority);
}
}
在需要使用的 controller 前加上 @PreAuthorize("@ex.hasAutority('dba')") 表示访问前的验证
补充
HttpSecurity
是 Spring Security 的关键部分,用于配置 Web 安全性的详细内容。以下是在 Spring Security 中常用的HttpSecurity
方法的概览:
authorizeRequests()
:开始请求级安全配置,允许你指定 URL 访问规则。
antMatchers(String... antPatterns)
:使用 Ant 风格的路径模式定义安全限制。
access(String)
:设置访问特定路径所需的权限表达式。
hasAuthority(String)
、hasAnyAuthority(String...)
:指定用户必须具有的权限以访问特定路径。
hasRole(String)
、hasAnyRole(String...)
:指定用户必须具有的角色才能访问特定路径。这通常自动添加 “ROLE_” 前缀。
formLogin()
:启用基于表单的身份验证。
loginProcessingUrl(String)
:定义处理登录请求的 URL。
permitAll()
:允许所有用户(即使未经认证)访问使用antMatchers
定义的路径。
anonymous()
表示可以被未登录的用户(即匿名用户)访问。
anyRequest()
:应用于所有剩余的 URL。
authenticated()
:要求在处理给定的请求之前进行身份验证,即,允许认证过的用户访问。
denyAll()
:无条件拒绝所有访问。
csrf()
:用于配置 CSRF(跨站请求伪造)保护。
disable()
:禁用csrf
保护或其他配置。
exceptionHandling()
:配置异常处理。
accessDeniedHandler(AccessDeniedHandler)
:自定义处理访问被拒绝的情况的策略。
httpBasic()
:启用 HTTP 基本认证。
logout()
:配置注销功能。
logoutUrl(String)
:设置触发注销操作的 URL。
logoutSuccessUrl(String)
:注销成功后重定向的 URL。
and()
:表示我们已经完成了授权相关的配置,并且需要添加一些其他的 HTTP 配置。
not()
:对其他访问结果取反。
cors()
:配置跨源资源共享(CORS)。
headers()
:配置各种 HTTP 头以增强安全性。
rememberMe()
:启用"记住我"的功能。
sessionManagement()
:配置会话管理。
maximumSessions(int)
:限制同一用户的并发会话数。addFilter(Filter filter):添加过滤器在链中对应的还有 Before 和 After 方法。
除了上述方法,还有一些专门的配置器可以针对不同的模块来配置
HttpSecurity
,如 OAuth2, SAML, LDAP 等。这些
HttpSecurity
方法通常会以链式调用的方式被调用,并最终构成一个安全配置顺序,定义了整个应用的安全策略。这种方法的组合几乎可以支持所有标准的 Web 安全需求。
Spring Security 是一个功能强大且灵活的框架,可帮助开发人员轻松实现应用程序的安全性需求。它提供了丰富的功能和配置选项,可以适应各种安全场景和要求。