文章目录
SpringBoot 整合 SpringSecurity —— Basic 模式
步骤一:导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yinxy</groupId>
<artifactId>test-spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-spring-security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
步骤二:创建可被访问的接口
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/add")
public String add() {
return "add";
}
@GetMapping("/delete")
public String delete() {
return "delete";
}
@GetMapping("/update")
public String update() {
return "update";
}
@GetMapping("/query")
public String query() {
return "query";
}
}
步骤三:配置 SpringSecurity
@EnableWebSecurity // 开启 SpringSecurity 的 Web 支持
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
// 在这里设置用户的信息以及他们的权限
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 添加用户
.withUser("admin").password("admin").authorities("add", "delete", "update", "query").and()
.withUser("add").password("add").authorities("add").and()
// 在 Spring 5.0 之后,必须要设置 PasswordEncoder,要不然在登录过程中会报错
// 这里选用的 PasswordEncoder 是不做任何操作的一种现实,仅在我们学习的时候可以这么使用
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
// 为 http 请求设置需要的权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 添加路径匹配规则
// e.g. 访问 "/user/add" 这个接口时,必须要有 "add" 这个权限
.antMatchers("/user/add").hasAuthority("add")
.antMatchers("/user/delete").hasAuthority("delete")
.antMatchers("/user/update").hasAuthority("update")
.antMatchers("/user/query").hasAuthority("query")
.antMatchers("/**").fullyAuthenticated().and()
// 启用 Basic 模式
.httpBasic();
}
}
步骤四:访问接口
http://localhost:8080/user/add ,可以发现出现了一个弹出框,这就是 Basic
模式,利用浏览器的弹出框实现的认证授权。
输入用户名 add
密码 add
就可以访问目标接口了。
但是如果你访问其他的路径 http://localhost:8080/user/update,就会发现请求被拦截了。至于为什么显示的如下内容,是因为我们没有配置 error 这个页面导致的。如果将换成 admin 账号的话,4 个接口就都可以被访问了。
SpringBoot 整合 SpringSecurity —— Form 表单模式
在实际中 Basic
模式我们很少会用到,相较之下更常用的是 Form
表单模式。也就是出现一个登录的页面,让用户输入账号和密码。由于 SpringSecurity 框架设计的非常好,所以换一种模式也是特别简单的事情,只需要改动一行代码即可。
步骤一:修改配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("add")
.antMatchers("/user/delete").hasAuthority("delete")
.antMatchers("/user/update").hasAuthority("update")
.antMatchers("/user/query").hasAuthority("query")
.antMatchers("/**").fullyAuthenticated().and()
// 启用 Basic 模式
// .httpBasic();
// 启用 Form 表单模式
.formLogin();
}
步骤二:访问路径
http://localhost:8080/user/add,可以发现我们跳转到了 http://localhost:8080/login 这个路径,而这个路径在我们的代码中是没有声明的,并且这个页面也不是我们提供的,都是由 Spring Security
内部实现的。在老版本的 Spring Security
中这个登录界面并不好看。
开启记住我功能
在登录一些网站的时候,都会有 记住我 让用户进行勾选,选中之后,重启了浏览器再打开这个页面,就不需要用户再次登录了。
步骤一:修改配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("add")
.antMatchers("/user/delete").hasAuthority("delete")
.antMatchers("/user/update").hasAuthority("update")
.antMatchers("/user/query").hasAuthority("query")
.antMatchers("/**").fullyAuthenticated().and()
// 启用 Basic 模式
// .httpBasic();
// 启用 Form 表单模式
.formLogin().and()
// 开启记住我功能
.rememberMe();
}
步骤二:访问路径
http://localhost:8080/user/add,可以看到已经有了 RememberMe 的复选框了,输入用户名密码,然后勾选记住我,登录成功后,重启浏览器就可以发现不需要重新登录了。
实现原理:RememberMe 的实现原理就是通过 Cookie,将会话ID保存到用户本地。
自定义登录页面
在正常情况下,我们会在登录界面上加很多东西,例如 Logo 之类的,这时候 SpringSecurity
自带的页面就已经不能满足我们的需求了,所以我们需要自定义登录页面。
步骤一:添加登录页面跳转接口
@Controller
public class IndexController {
@GetMapping("/login")
public String login() {
return "login";
}
}
步骤二:编写登录页面 login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form method="post" action="/login">
<label>
<input type="text" name="username" />
</label> 用户名
<label>
<input type="password" name="password" />
</label> 密码
<input type="submit">
</form>
</body>
</html>
步骤三:修改配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasAuthority("add")
.antMatchers("/user/delete").hasAuthority("delete")
.antMatchers("/user/update").hasAuthority("update")
.antMatchers("/user/query").hasAuthority("query")
.antMatchers("/login").permitAll()
.antMatchers("/**").fullyAuthenticated().and()
.formLogin()
// 设置登录页面的路径
.loginPage("/login").and().csrf().disable();
}
步骤四:访问任意路径
http://localhost:8080/user/add,就会跳转到我们自定义的登录页面了。
项目结构
SpringSecurity 整合数据库
在实际的项目中,我们不会将用户信息和路径需要的权限直接写在代码中,而是从数据库中查询。所以在这一小节中我就来教大家如何将 SpringSecurity 整合数据库。
步骤一:创建数据库表
我选用的权限模型是 RBAC 模型。
CREATE TABLE `user` (
`id` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(32) NOT NULL DEFAULT '' COMMENT '用户',
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `role` (
`id` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_name` varchar(64) NOT NULL COMMENT '角色名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`url` varchar(255) DEFAULT NULL COMMENT '路径',
`perm_tag` varchar(255) DEFAULT NULL COMMENT '权限标签',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `user_role` (
`id` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_id` int(32) DEFAULT NULL COMMENT 'roleid',
`user_id` int(32) DEFAULT NULL COMMENT 'userid',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='角色用户表';
CREATE TABLE `role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
`permission_id` bigint(20) DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
步骤二:动态设置接口需要的权限
@Autowired
private PermissionMapper permissionMapper;
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
registry = http.authorizeRequests();
// 从数据库中获取所有的权限
List<PermissionEntity> permissions = permissionMapper.findAllPermission();
permissions.forEach(p -> {
// 动态设置接口需要的权限
registry.antMatchers(p.getUrl(), p.getPermTag());
});
registry.antMatchers("/login").permitAll()
.antMatchers("/**").fullyAuthenticated().and()
.formLogin().and().csrf().disable();
}
步骤三:用户实体类实现 UserDetails 接口
public class UserEntity implements UserDetails, Serializable {
private static final long serialVersionUID = -1677058108638628245L;
@TableId
private Long id;
// 用户名
private String username;
// 密码
private String password;
private List<PermissionEntity> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// 账号是否没有过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账号是否没有被冻结
@Override
public boolean isAccountNonLocked() {
return true;
}
// 账号密码是否没有过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账号是否启用
@Override
public boolean isEnabled() {
return true;
}
// get 和 set 方法
}
步骤四:权限实体类实现 GrantedAuthority 接口
public class PermissionEntity implements Serializable,
GrantedAuthority {
private static final long serialVersionUID = 8313212656939191264L;
@TableId
private Long id;
// 接口路径
private String url;
// 访问该接口路径需要的权限 Tag
private String permTag;
@Override
public String getAuthority() {
return permTag;
}
// get 和 set 方法
}
步骤五:创建获取能获取用户信息的服务类
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userMapper.findByUsername(username);
if (user == null) {
return null;
}
// 通过用户名获取所有的权限
List<PermissionEntity> permissions = userMapper.findPermissionByUsername(username);
user.setAuthorities(permissions);
return user;
}
}
步骤六:创建用户密码加密规则
这里大家可以使用任意的加密算法,我这里采用的是 MD5,使用的是 hutool
工具集。
public class MyPasswordEncoder implements PasswordEncoder {
private final Digester DIGESTER = MD5.create().setSalt("123456".getBytes());
@Override
public String encode(CharSequence rawPassword) {
return DIGESTER.digestHex(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(DIGESTER.digestHex(rawPassword.toString()));
}
}
步骤七:实现动态查询用户信息
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new MyPasswordEncoder());
}
小结:经过这 7 步就实现了 接口权限 和 用户权限 的动态获取。