1.SpringSecurity简介
Spring Security,作为Spring框架的重要组件,是一个功能全面且高度可定制的安全框架。它利用Spring的IoC/DI和AOP机制,提供了声明式的安全访问控制,显著减少了开发人员在实现系统安全时所需编写的重复代码。与javaEE的Servlet和EJB规范相比,Spring Security更加贴近企业级应用场景,其配置和功能是WAR/EAR级别独立的,因此更换服务器环境时无需大量重新配置。 Spring Security支持多种认证和授权机制,能够轻松保护REST、WebSocket等端点,是Java应用安全性的理想选择。
2.应用场景
2.1 对已有项目添加认证功能
对于已经存在的项目,尤其是那些包含Web访问控制页面的技术,如Solr的web管理页面,安全性往往是一个重要的考虑因素。这些页面若未设置适当的认证机制,如Solr的默认设置允许任何知道其IP和端口的人进行访问,就可能导致数据泄露或其他安全问题。在这种情况下,尽管项目可能不是基于Maven构建的,我们仍可以尝试集成Spring Security来增强安全性。通过Spring Security,我们可以为这些页面添加用户认证功能,确保只有授权的用户才能访问敏感数据。这种思想不仅适用于Solr,也适用于我们未来可能遇到的其他技术。
2.2 对常规项目
对于常规项目,尤其是那些需要权限控制的项目,Spring Security同样是一个理想的选择。无论是一个全新的项目,还是正在开发中的项目,只要需要实现用户认证、角色授权等安全功能,都可以考虑使用Spring Security。其高度可定制的特性使得它能够适应各种复杂的安全需求,为项目提供坚实的安全保障。通过Spring Security,我们可以为项目构建一套完善的安全机制,确保系统的稳定性和数据的安全性。
3.创建一个SpringSecurity项目
创建一个springboot项目然后加入依赖
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.访问页面
导入spring-boot-starter-security启动器,默认拦截全部请求,如果用户没登录,跳转到内置登录页面。
@Controller
public class TextController {
@RequestMapping("/text")
@ResponseBody
public String text(){
return "success";
}
}
启动springboot项目
默认的username为user,password打印在控制台中
登陆成功后
可以自己设置一个账号密码 在application.yml
spring:
security:
user:
name: zhangsan
password: 123456
4.PasswordEncoder密码解析器解释
Spring Security要求容器中必须有PasswordEncoder实例。
接口介绍
-
encode():
- 功能:该函数负责将输入的参数按照特定的解析规则进行编码或转换。
- 输入:通常是一个需要被编码的字符串或数据。
- 输出:返回编码后的结果,这个结果可能是加密后的密码、散列值、或其他任何基于特定规则转换后的形式。
- 目的:通常用于增加数据的安全性,例如密码存储前的加密或散列处理。
-
matches():
- 功能:该函数用于验证两个密码是否匹配。它比较从存储中获取的编码后的密码与编码后提交的原始密码是否相同。
- 输入:
- 第一个参数:用户提交的原始密码,经过encode()函数处理后的结果。
- 第二个参数:从存储中获取的已经编码的密码。
- 输出:
- 如果两个密码匹配,则返回true。
- 如果不匹配,则返回false。
- 目的:用于验证用户输入的密码是否与存储中的密码一致,确保用户的身份和权限。
-
upgradeEncoding():
- 功能:该函数检查已编码的密码是否可以被进一步解析或升级以达到更高的安全性。
- 输入:通常是一个已经编码的密码。
- 输出:
- 如果密码能够再次进行解析或升级以达到更高的安全性,则返回true。
- 否则,返回false。
- 目的:用于评估现有密码的安全性,并考虑是否需要升级到更安全的编码方式,以应对新的安全威胁或标准。
内置解析器介绍
BCryptPasswordEncoder简介
Spring Security官方推荐的密码解析器。可以通过strength控制加密强度,默认10。
5.自定义认证流程
自定义登录逻辑
编写数据库
CREATE TABLE `tb_permission` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL COMMENT '权限名称',
`url` varchar(255) DEFAULT NULL COMMENT '请求地址',
`parent_id` int DEFAULT NULL COMMENT '父权限主键',
`type` varchar(24) DEFAULT NULL COMMENT '权限类型, M - 菜单, A - 子菜单, U - 普通请求',
`permit` varchar(128) DEFAULT NULL COMMENT '权限字符串描述,如:user:list 用户查看权限 user 用户权限 user:insert 用户新增权限 等',
`remark` text COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL COMMENT '角色名称',
`remark` text COMMENT '角色描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_role_permission` (
`role_id` int DEFAULT NULL COMMENT '角色外键',
`permission_id` int DEFAULT NULL COMMENT '权限外键'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL COMMENT '姓名',
`username` varchar(32) DEFAULT NULL COMMENT '用户名',
`password` varchar(128) DEFAULT NULL COMMENT '密码',
`remark` text COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_user_role` (
`user_id` int DEFAULT NULL COMMENT '用户外键',
`role_id` int DEFAULT NULL COMMENT '角色外键'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
添加mybatis相关依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
application.yml
spring:
security:
user:
name: zhangsan
password: 123456
datasource:
password: 123456
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
定义实体类
@Data
public class Permission implements Serializable {
/*主键*/
private Integer id;
/*权限名称*/
private String name;
/*请求地址*/
private String url;
/*父权限主键*/
private Integer parentId;
/*分类 M - 菜单, A - 子菜单, U - 普通请求*/
private String type;
/*权限字符串*/
private String permit;
/*描述*/
private String remark;
}
@Data
public class Role implements Serializable {
/*主键*/
private Integer id;
/*角色名称*/
private String name;
/*角色描述*/
private String remark;
}
@Data
public class User implements Serializable {
/*主键*/
private Integer id;
/*姓名*/
private String name;
/*用户名*/
private String username;
/*密码*/
private String password;
/*描述*/
private String remark;
}
mapper层
@Mapper
public interface PermissionMapper {
@Select("select p.id,p.name,p.url,p.parent_id,p.type,p.permit,p.remark from tb_permission p left join tb_role_permission rp on p.id = rp.permission_id left join tb_user_role ur on ur.role_id = rp.role_id where ur.user_id = #{userId}")
List<Permission> selectPermissionByUser(Integer userId);
}
@Mapper
public interface RoleMapper {
@Select("select r.id,r.name,r.remark from tb_role r left join tb_user_role ur on r.id = ur.role_id where ur.user_id = #{userId}")
List<Role> selectRoleByUserId(Integer userId);
}
@Mapper
public interface UserMapper {
@Select("select id,name,username,password,remark from tb_user where username = #{username}")
User selectByUserName(String username);
}
定义密码解析器
public class MyPasswordEncoder implements PasswordEncoder
alt加回车就能生成出来了
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
System.out.println("自定义密码解析器 - encode方法执行");
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
System.out.println("自定义密码解析器 - matches方法执行");
// 先使用encode方法,用相同的加密策略,加密明文,再对比密文。
return encode(rawPassword).equals(encodedPassword);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return PasswordEncoder.super.upgradeEncoding(encodedPassword);
}
此类型对象必须由spring容器管理
configuration创建对象
@Configuration
public class MySecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder(){
return new MyPasswordEncoder();
}
}
service层
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("自定义登录服务 - loadUserByUsername方法执行");
// 根据用户名查询用户
User user = userMapper.selectByUserName(username);
// 判断用户名是否存在
if(user == null){
System.out.println("用户名:" + username + " 不存在");
// 用户名不存在
throw new UsernameNotFoundException("用户名或密码错误");
}
// 返回UserDetails接口类型对象
org.springframework.security.core.userdetails.User result =
new org.springframework.security.core.userdetails.User(
username, // 登录用户的用户名
user.getPassword(), // 登录用户的密码,是服务端保存的密文
AuthorityUtils.NO_AUTHORITIES // 工具提供的无权限空集合
);
return result;
}
}
登录测试
自定义认证流程
强散列密码解析器
Security框架,提供若干密码解析器实现类型。其中 BCryptPasswordEncoder 叫强散列加密。
可以保证相同的明文,多次加密后,密文有相同的散列数据,而不是相同的结果。
匹配时,是基于相同的散列数据做的匹配。
我们来实验一下,先创建一个text
public class TestPasswordEncoder {
public static void main(String[] args) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
System.out.println(passwordEncoder.encode("123"));
System.out.println(passwordEncoder.matches("123","$2a$10$wXkhWBkziA7ynSoE0ZZZO.Pl5umasbDtBpe4a.qwo/QZ.O3boZL7C"));
}
}
他是保证相同的散列数据而不是结果,我把任何一个结构值保存在数据库都可以得到相同的结果数据
更改configuration文件
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();可以设置加密强度4-31 数字越大速度越慢强度越高
编写登录页面
spring boot3 + jakartaEE + security 6 ,修改了自定义配置策略。
spring boot2 + javaEE + security 5 版本的 WebSecurityConfigure 自动装配策略,改为 Configuration + Bean 对象配置策略。
package com.example.securitytext.config;
import com.example.securitytext.encoder.MyPasswordEncoder;
import com.example.securitytext.handler.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
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.configurers.FormLoginConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class MySecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
Customizer<FormLoginConfigurer<HttpSecurity>> customizer = new Customizer<FormLoginConfigurer<HttpSecurity>>() {
@Override
public void customize(FormLoginConfigurer<HttpSecurity> configurer) {
// 具体的认证配置
configurer.loginPage("/login");
}
};
security.formLogin(customizer);
security.authorizeRequests()
.requestMatchers("/login","/userLogin").permitAll()
.anyRequest().authenticated();
security.csrf().disable();
return security.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
package com.example.securitytext.Controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.annotation.RequestScope;
@Controller
public class LoginViewController {
@RequestMapping("/login")
public String toLogin(){
return "login";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div style="width: 600px; margin: auto">
<form action="" 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>
<input type="submit" value="登录">
</td>
</tr>
</table>
</form>
</div>
</body>
</html>
自定义登录请求地址
在上面的html的代码中我们设置一下请求地址
<form action="/userLogin" method="post">
然后在MySecurityConfiguration中添加地址
自定义登录请求名称
如下
自定义登录后处理
.defaultSuccessUrl("/main") // 设置默认的认证成功后跳转地址,仅在直接访问登录页面时,生效。
.successForwardUrl("/main") // 设置认证成功后转发地址。全局生效。
另一种处理方式
.successHandler(new MyAuthenticationSuccessHandler("/main",true));
认证成功后处理代码逻辑
package com.bjsxt.handler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException;
/**
* 认证成功后,处理代码逻辑。
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
/*登录成功后的访问地址*/
private String url;
/*是否是重定向*/
private boolean isRedirect;
public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {
this.url = url;
this.isRedirect = isRedirect;
}
/**
* 认证成功后,具体执行的代码
* @param authentication 认证成功后的用户主体对象。
* 包含登录用户的个人信息和权限列表
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if(isRedirect){
// 重定向
response.sendRedirect(url);
}else{
// 请求转发
request.getRequestDispatcher(url).forward(request, response);
}
}
}
自定义登录后处理
security框架中,认证失败后,默认重定向到 登录页面请求地址?error
此默认信息在 AbstractAuthenticationFilterConfigurer 类型中定义的。
.failureUrl("/loginFail") //认证失败后转发地址 重定向处理
.failureForwardUrl("/loginFail")//认证失败后转发地址
.failureHandler(new MyAuthenticationFailureHandler("/loginFail")); // 设置认证失败后的处理代码逻辑, AuthenticationFailureHandler
@Override
public void customize(FormLoginConfigurer<HttpSecurity> configurer) {
// 具体的认证配置
configurer.loginPage("/login") // 设置登录页面访问地址。默认是 /login。 必须是get请求。 自定义后,提供控制器+视图
.loginProcessingUrl("/userLogin") // 设置登录请求地址,此请求必须是post。默认 /login
.usernameParameter("name") // 设置请求参数名称, 用户名
.passwordParameter("psd") // 设置请求参数名称, 密码
.defaultSuccessUrl("/main").successForwardUrl("/main")
.successHandler(new MyAuthenticationSuccessHandler("/main",true))
.failureUrl("/loginFail") //认证失败后转发地址 重定向处理
.failureForwardUrl("/loginFail")//认证失败后转发地址
.failureHandler(new MyAuthenticationFailureHandler("/loginFail")); // 设置认证失败后的处理代码逻辑, AuthenticationFailureHandler
}
};
控制器中
@RequestMapping("/loginFail")
public String toFail(){
return "fail";
}
}
html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>用户名或密码错误,请重新<a href="/login">登录</a></h3>
</body>
</html>
failureHandler
设置认证失败后的处理代码逻辑,新建MyAuthenticationFailureHandler
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
自定义认证
remember_me
Security认证附加功能, remember me。 可以让同一个客户端,减少登录次数。
1. 登录请求参数,必须新增一个 remember-me,值是true,代表记住我。
参数名称可以通过配置修改。
2. 使用配置的方式,提供必要信息。
设置认证配置,配置rememberme相关信息
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService myUserDetailsService;
Customizer<RememberMeConfigurer<HttpSecurity>> rememberMe = new Customizer<RememberMeConfigurer<HttpSecurity>>() {
@Override
public void customize(RememberMeConfigurer<HttpSecurity> configurer) {
configurer
.tokenRepository(persistentTokenRepository()) // 设置保存记住我数据的具体对象
.rememberMeParameter("remember-me") // 请求参数中,记住我参数名,默认 remember-me
.rememberMeCookieName("REMEMBER-ME") // 记住我服务器端把登录数据保存在数据库,客户端通过cookie记录数据库唯一信息,默认 remember-me
//.rememberMeCookieDomain("localhost") // cookie的domain,默认自动解析
.tokenValiditySeconds(18000) // cookie保存时长,单位是秒。默认1800秒
.userDetailsService(myUserDetailsService); // 设置自定义UserDetailsService接口实现对象
}
};
security.rememberMe(rememberMe);
/**
* 创建一个Bean对象
* 这个bean对象,使用Security框架提供的实现类型即可。
* JdbcTokenRepositoryImpl - 基
于DataSource数据源连接池,访问指定的数据库
* 把认证成功的,需要rememberMe的用户数据保存到数据库
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl repository =
new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
// 初始化参数,仅第一次启动当前项目时设置为true。后续再次启动修改为false
// 如果设置为true,启动时,自动连接数据库,创建表格。表格就是保存rememberMe数据的
repository.setCreateTableOnStartup(false);
return repository;
}
login页面添加
<tr>
<td colspan="2">
<input type="checkbox" name="remember-me" value="true">记住我
</td>
</tr>
退出登录
Security框架中,默认提供了退出登录的功能。
此功能请求地址是 /logout。 此为默认值,可以通过配置修改。
直接请求 /logout ,自动实现退出登录逻辑。
退出登录时,会清除应用内存中的登录用户主体信息,销毁会话对象,删除rememberMe信息。
如果没有特定的要求,直接访问 /logout 地址即可。
退出登录成功后,默认访问的地址是 登录页面请求地址?logout /login?logout
退出登录成功后的跳转地址,在 logoutconfigure类型中定义的。
main.html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>看到了,就是登录成功了。</h1> <a href="/logout">退出登录</a>
</body>
</html>
退出登录配置 MySecurityConfiguration
//退出登录配置
Customizer<LogoutConfigurer<HttpSecurity>> logout = new Customizer<LogoutConfigurer<HttpSecurity>>() {
@Override
public void customize(LogoutConfigurer<HttpSecurity> configurer) {
configurer
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.addLogoutHandler(new MyLogoutHandler());
}
};
security.logout(logout);
MyLogoutSuccessHandler
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出登录成功后的处理");
// 销毁会话,清空缓存等。
request.getSession().invalidate(); // 销毁会话
authentication.setAuthenticated(false); // 设置未登录状态
response.sendRedirect("/login"); // 重定向
}
}
Logouthandler
public class MyLogoutHandler implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 一般做额外处理,比如保存会话中的某些attribute到数据库
// 比如保存会话中的某数据到文件。
// 比如记录日志数据等。
System.out.println("额外的退出登录逻辑。");
}
}
授权管理
修改userDetailsService 提供权限列表
Security框架中的授权管理需要认证时先提供用户的权限列表。
1. 先修改 UserDetailsService 中的代码逻辑,增加认证用户权限数据。
注意: Security框架对权限的管理是基于请求地址+权限字符串描述实现的。
请求地址和权限字符串描述,都是字符串数据。
Security框架也可以处理角色权限。基于角色名称管理权限。角色名称同样是字符串,
为了区分角色名称和前学字符串描述的不同,要求角色名称必须已 ROLE_ 开头。
2. 通过配置类型,做初步权限控制。
MyUserDetailsServiceImpl
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
//开始查询已经登录用户的权限集合
//查询角色
List<Role> roles = roleMapper.selectRoleByUserId(user.getId());
//查询权限
List<Permission> permissions = permissionMapper.selectPermissionByUser(user.getId());
// 定义一个字符串泛型的List集合对象
List<String> authorities = new ArrayList<>();
for(Role role : roles){
// 角色名称纳入的权限管理,必须增加前缀 ROLE_
authorities.add("ROLE_" + role.getName());
}
for(Permission permission : permissions){
// 权限字符串描述,不需要特定的任何前后缀。
authorities.add(permission.getPermit());
}
修改一下AuthorityUtils.NO_AUTHORITIES // 工具提供的无权限空集合、、
修改一下AuthorityUtils.NO_AUTHORITIES
NO_AUTHORITIES
为
AuthorityUtils.createAuthorityList(authorities) // 工具提供的无权限空集合
请求地址映射实现权限管理
MySecurityConfiguration
/*
* requestMatchers 方法有多个重载 overload 方法。常用的是
* requestMatchers(String... urls) 针对参数请求地址做映射匹配
* requestMatchers(Method method, String... urls) 针对参数请求地址+固定请求方式做映射匹配
* HttpMethod.GET .POST .PUT .DELETE 等。仅能指定一种请求方式
* anyRequest 代表一切请求地址映射。相当于 requestMatchers("/**")
* 注意,此方法,必须在最后调用。
*/
security.authorizeRequests()
//.requestMatchers(HttpMethod.POST,"/js/**") //只有post可以请求
.requestMatchers("/js/**")// 设置请求地址是 /js/**时,可以随意访问
.requestMatchers("/login","/userLogin","/loginFail").permitAll()
.anyRequest().authenticated();// 访问其他所有地址时,必须认证成功后可以访问。
在spriongboot项目中,如果有web开发逻辑,静态可以直接访问资源,统一定义在class path:/static/ 文件夹中 此文件夹的资源全是静态资源 比如 html css js 可以直接访问
新建一个test.js文件
框架基础权限管理方法
Security框架授权管理中的基础权限,都需要配合路径地址匹配方案。
anonymous - 必须未登录才可以访问
authenticated - 必须已登录才可以访问,包含rememberMe登录方式
fullyAuthenticated - 必须完整登录才可以访问,不可以使用rememberMe方式登录,敏感操作中使用,如消费,改密码
rememberMe - 必须使用rememberMe方式才可以访问,不推荐使用。
permitAll - 无权限校验,随意访问
denyAll - 不可以访问,开发了新功能,没有经过完整测试,代码已上线,不想删除或注释未测试代码时,可选用。
例:
输入网址就能访问
登录后访问报错403
基于角色和权限描述的管理方法
Security框架中的相对细致的权限管理。包括:针对角色的权限管理、针对权限描述的权限管理、针对客户端访问IP的权限管理
角色管理
hasRole - 登录用户权限列表中,是否存在参数角色。参数是角色名称,不含前缀 ROLE_
hasAnyRole - 登录用户权限列表中,有参数字符串数组的任意一个角色,都可以访问。参数是角色名称,不含前缀 ROLE_
权限描述管理
hasAuthority - 登录用户权限列表中,是否存在权限描述字符串。
hasAnyAuthority - 有参数字符串数组中任意权限描述,都可以访问
客户端IP管理
hasIpAddress - 访问客户端IP地址限制。相对使用较少。基本见于内部地址过滤。
比如:京东工作人员登录后台管理系统时,必须处于京东工作区域中,且所有的网络通过统一的一个路由或交换器连接服务器
因为路由或交换器唯一,且IP固定,进行限制,代表仅工作区域电脑可以访问。
security.authorizeRequests()
.requestMatchers("/js/**").hasRole("超级管理员")
.requestMatchers("/login","/userLogin","/loginFail").permitAll()
.anyRequest().authenticated();
access权限管理方法
解读Security源码中的基础权限管理和角色、权限描述、IP权限管理
这些权限管理方法,其底层统一是 access 方法。
具体映射关系如下:
anonymous - access("anonymous")
authenticated - access("authenticated")
fullyAuthenticated - access("fullyAuthenticated")
rememberMe - access("rememberMe")
permitAll - access("permitAll")
denyAll - access("denyAll")
hasRole(角色名) - access("hasRole('ROLE_+角色名')")
hasAnyRole(角色名1,角色名2) - access("hasAnyRole('ROLE_+角色名1', 'ROLE_+角色名2')")
hasAuthority(权限) - access("hasAuthority('权限')")
hasAnyAuthority(权限1,权限2) - access("hasAnyAuthority('权限1', '权限2')")
hasIpAddress(IP) - access("hasIpAddress('IP')")
自定义统一权限管理
自定义一个类型,实现统一的权限管理策略。 是曾经很流行的编写方式。强制要求前端发起请求时,必须携带特定参数。
且参数数据可能变化,需要同步修改前端代码。现在的新项目中几乎不使用。
1. 定义一个接口,接口中定义唯一的方法。方法要求是
boolean 方法名(HttpServletRequest req, Authentication authentication)
public interface MyPermissionChecker {
boolean check(HttpServletRequest request, Authentication authentication);
}
实现类型的对象,必须被spring容器管理。
package com.example.securitytext.check.impl;
import com.example.securitytext.check.MyPermissionChecker;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
@Component
public class MyPermissionCheckerImpl implements MyPermissionChecker {
@Override
public boolean check(HttpServletRequest request, Authentication authentication) {
// 1. 获取本次请求的路径地址
String path = request.getServletPath();
// 判断请求地址是否直接放行
if("/login".equals(path) || "/userLogin".equals(path) || "/loginFail".equals(path)
|| path.startsWith("/js/")){
return true;
}
// 获取请求参数
String perm = request.getParameter("perm");
perm = (perm != null && perm.trim().length() > 0) ? perm.trim() : "none";
// 获取权限前,需要先判断是否已经登录
if(authentication != null && authentication.isAuthenticated()) {
// 已经登录
// 获取当前已认证登录的用户的权限
Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();
// 判断权限集合中是否存在当前请求需要的权限
if(authorities.contains(new SimpleGrantedAuthority(perm))){
// 有权限
return true;
}
}else{
// 未登录
return false;
}
// 没有权限
return false;
}
}
2. 编写配置信息
在Spring配置文件和配置类型中,都可以使用SpringEL表达式,
@bean对象名称, 可以从容器中获取bean对象。
@bean对象名称.方法名称, 可以调用对象的方法
可以通过某对象的property属性名,直接为方法传递参数。要求对象必须被spring容器管理。且对象信息为配置或管理
比如 Spring Security 框架中的 WebSecurityExpressionRoot。 类型命名中存在 Expression 的大部分都是SpringEL
可以直接访问属性的。
security.authorizeRequests()
.requestMatchers("/**").access("@myPermissionCheckerImpl.check(request, authentication)");
Security框架的access方法参数值,如果是 "true" 字符串,代表有权限; "false" 字符串,代码无权限
403相应状态处理
自定义代码处理403显示逻辑
1. 编写 AccessDeniedHandler 接口实现类。
2. 配置
MyAccessDeniedHandler
package com.example.securitytext.handler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println(accessDeniedException.getMessage());
// 通过响应输出流,向客户端输出一个友好的结果
response.setContentType("text/html; charset=UTF-8");
response.getWriter().println("<h3 style='color: blue'>权限不足,请联系管理员</h3>");
response.getWriter().flush();
}
}
MySecurityConfiguration
Customizer<ExceptionHandlingConfigurer<HttpSecurity>> handlingConfigurerCustomizer = new Customizer<ExceptionHandlingConfigurer<HttpSecurity>>() {
@Override
public void customize(ExceptionHandlingConfigurer<HttpSecurity> configurer) {
configurer.accessDeniedHandler(new MyAccessDeniedHandler());
}
};
权限管理注解
注解+少量配置,实现全部权限管理。
配置:提供部分固定的权限管理,如,登录页面、登录请求、登录错误、退出登录、静态资源放行等权限管理。
注解:提供各种具体服务的权限管理,如,用户查询、新增、修改、删除; 角色管理、 菜单权限管理等。
注解权限管理,必须
1. 在启动类型或可扫描到的Configuration配置类型上,增加注解 EnableGlobalMethodSecurity
给注解增加属性,开启你需要的权限管理注解。 securedEnabled = true, prePostEnabled = true
securedEnabled - 开启注解 @Secured 功能。用于基于角色的权限管理
prePostEnabled - 开启注解 @PreAuthorize 和 @PostAuthorize 注解。用于基于表达式字符串的权限管理
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
security.authorizeRequests()
//.requestMatchers("/**").access("@myPermissionCheckerImpl.check(request, authentication)");
.requestMatchers("/login", "/userLogin", "/loginFail").anonymous() // 访问 /login 地址时,不做认证授权验证。
.requestMatchers("/js/**", "/css/**", "/images/**").access("permitAll") // 设置请求地址是 /js/**时,可以随意访问
.requestMatchers("/logout").authenticated()
.anyRequest().authenticated(); // 访问其他所有地址时,必须认证成功后可以访问。
表达式字符串,就是 access 方法的参数字符串表达式,比如 "hasRole('ROLE_角色名称')"
注意,在低版本Security框架中,此注解不开启,直接使用 @Secured 等注解时,启动错误。
2. 在具体的服务方法上,通过注解实现权限的管理。一般注解写在控制器中的控制单元方法上。
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/add")
@PreAuthorize("hasAuthority('user:add')")
public String addUser(){
System.out.println("新增用户方法运行");
return "新增用户";
}
@RequestMapping("/list")
@Secured({"ROLE_超级管理员"/*, "ROLE_普通用户"*/})
public String listUsers(){
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
System.out.println("身份:" + authentication.getPrincipal());
System.out.println("凭证:" + authentication.getCredentials());
System.out.println("明细:" + authentication.getDetails());
System.out.println("权限:" + authentication.getAuthorities());
return "显示用户查询列表";
}
}
Thymeleaf在security的用法
在thymeleaf-extras-springsecurity6依赖中,
提供了一个新的thymeleaf属性名字空间,名字空间是 sec
名字空间中包含的标签属性有:
sec:authentication="" , 此属性是访问Security框架管理的内存中的已登录用户主体对象。此对象类型是 Authentication 接口类型。
具体的类型是 UsernamePasswordAuthenticationToken 类型。
属性中直接写 UsernamePasswordAuthenticationToken 类型中的property属性名。可以获取属性的值。
sec:authorize="", 此属性类似权限管理注解 PreAuthorize 。属性的值就是access方法可识别的所有权限表达式。
当权限表达式满足时,显示属性所在标签;不满足权限表达式,此标签不存在。
依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<h3>身份:<span sec:authentication="principal"></span></h3>
<h3>凭证:<span sec:authentication="credentials"></span></h3>
<h3>权限列表:<span sec:authentication="authorities"></span></h3>
<h3>名字:<span sec:authentication="name"></span></h3>
<h3>明细:<span sec:authentication="details"></span></h3>
<hr>
<a href="/user/list" sec:authorize="hasAuthority('user:list')">查询用户</a>
<a href="/user/add" sec:authorize="hasAuthority('user:add')">新增用户</a>
<a href="/toRegister" sec:authorize="hasAuthority('reg:toRegister')">去注册</a>>
Java代码访问Security中的已登录用户数据
Security认证流程结束后,会把认证登录成功的用户数据封装成Authentication类型对象,
并保存在内存中(HttpSession会话中)。
但是此数据是由框架自动保存的。那么attribute名字,开发者不知道。
Security框架提供了一个工具类型 SecurityContextHolder,
SecurityContextHolder中有静态方法 SecurityContext getContext(),
返回的SecurityContext,就是当前请求对应的会话中保存的登录用户上下文对象。
SecurityContext类型中有方法, Authentication getAuthentication()
可以通过Authentication对象,获取需要的用户数据。
@RequestMapping("/list")
@Secured({"ROLE_超级管理员", "ROLE_普通用户"})
public String listUsers(){
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
System.out.println("身份:" + authentication.getPrincipal());
System.out.println("凭证:" + authentication.getCredentials());
System.out.println("明细:" + authentication.getDetails());
System.out.println("权限:" + authentication.getAuthorities());
return "显示用户查询列表";
}
CSRF令牌
开启CSRF之后,在登录和退出的时候,必须发送POST请求。
请求中必须额外提供一个请求参数,参数名是 _csrf, 参数的值,由服务器提供。
在thymeleaf等视图逻辑中,使用表达式获取令牌值。
服务器返回的令牌值,保存在请求作用域中,命名是_csrf,数据保存在对象属性token中。
每一次通过服务器进行视图跳转的时候,都有一个令牌生成,且通过请求作用域传递到视图。
<!DOCTYPE html>
<html lang="en" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/js/jquery.js"></script>
<script>
function doLogout(){
// 获取令牌数据
var token = $("#token").val();
// post请求,退出登录
$.post("/logout",{"_csrf": token}, function (data){
alert(data);
// 退出后,跳转到登录页面
window.location.href="/login"
});
}
</script>
</head>
<body>
<input type="hidden" id="token" th:value="${_csrf.token}">
<h3>令牌是:<span th:text="${_csrf.token}"></span></h3>
<hr>
<h3>身份:<span sec:authentication="principal"></span></h3>
<h3>凭证:<span sec:authentication="credentials"></span></h3>
<h3>权限列表:<span sec:authentication="authorities"></span></h3>
<h3>名字:<span sec:authentication="name"></span></h3>
<h3>明细:<span sec:authentication="details"></span></h3>
<hr>
<a href="/user/list" sec:authorize="hasAuthority('user:list')">查询用户</a>
<a href="/user/add" sec:authorize="hasAuthority('user:add')">新增用户</a>
<a href="/toRegister" sec:authorize="hasAuthority('reg:toRegister')">去注册</a>
<h3>看到了,就是登录成功了。<!--<button onclick="doLogout()">退出登录</button>-->
<form action="/logout" method="post">
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<input type="submit" value="退出登录">
</form>
</h3>
</body>
</html>