Spring Security Tutorial (安全访问,登陆验证,权限) - SpringBoot集成Spring Security
文章目录
1:简单的登录验证
让我们花一些时间简单地配置 Security 。
在 pom.xml 文件中添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
现在编译并运行您的项目
您可以发现密码已经打印出来,打开浏览器并输入localhost,如下所示。
简单吗?如果您不觉得它耐嚼,那么恭喜您,您已经开始了,安全地拜访,欢呼🍻。
默认账号:user
默认密码:查看控制台输出的信息
2:保护Web应用程序的安全
保持您的热情,让我们继续。
创建一个登录页面。(login.html)
<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" xmlns:th = "http://www.thymeleaf.org"
xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if = "${param.error}">
用户名和密码无效。
</div>
<div th:if = "${param.logout}">
您已注销。
</div>
<form th:action = "@{/login}" method = "post">
<div>
<label> User Name : <input type = "text" name = "username"/> </label>
</div>
<div>
<label> Password: <input type = "password" name = "password"/> </label>
</div>
<div>
<input type = "submit" value = "Sign In"/>
</div>
</form>
</body>
</html>
如果你的编译器报错Cannot resolve ‘param’,就把 xmlns:th = "http://www.thymeleaf.org"
修改成 xmlns:th ="http://thymeleaf.org"
创建一个退出页面:(logout.html)
<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" xmlns:th = "http://www.thymeleaf.org"
xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>logout</title>
</head>
<body>
<form th:action = "@{/logout}" method = "post">
<input type = "submit" value = "Sign Out"/>
</form>
</body>
</html>
创建一个Web安全配置文件,该文件用于保护您的应用程序通过使用基本身份验证来访问HTTP端点。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER");
}
}
configure(HttpSecurity)方法定义一些URL路径应受到保护,哪些不应受到保护。
例如,如上所述,这意味着将“ /”和“ / home”路径配置为不需要任何身份验证。所有其他路径必须经过验证。
loginPage()指定登录页面,未登录的用户将在此处被拦截并替换。
在configureGlobal方法中,我们将用户设置为“ user”,密码为“ password”,角色为“ USER”。它将单个用户设置在内存中。
并添加{noop}
设置密码存储格式。
如果您没有配置Spring MVC并设置视图控制器,就让我们配置一个 Controller。
@Controller
public class ViewController {
@RequestMapping("/login")
public String login(HttpServletRequest request) {
return "login";
}
@RequestMapping("/logout")
public String logout(HttpServletRequest request) {
return "logout";
}
}
在浏览器中输入 localhost:8080/logout
输入刚才设置账号和密码,登录
恭喜你!您已经开发了一个受Spring Security保护的简单Web应用程序。🍻
3:注册密码加密
注册时,我们在这里使用了mybatis,而没有解释详细的步骤,我们仅谈论使用spring security提供的加密方法。
清单 1:BCryptPasswordEncoder
该BCryptPasswordEncoder实现使用广泛支持的bcrypt算法对密码进行哈希处理。为了使其更能抵抗密码破解,bcrypt故意降低了速度。与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
// 创建强度为16的编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
清单 2:Argon2PasswordEncoder
该Argon2PasswordEncoder实现使用Argon2算法对密码进行哈希处理。Argon2是“ 密码哈希竞赛”的获胜者。为了克服自定义硬件上的密码破解问题,Argon2是一种故意慢速的算法,需要大量内存。与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。如果Argon2PasswordEncoder需要BouncyCastle,则为当前实现。
// 创建具有所有默认设置的编码器
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
清单 3:Pbkdf2PasswordEncoder
该Pbkdf2PasswordEncoder实现使用PBKDF2算法对密码进行哈希处理。为了消除密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。当需要FIPS认证时,此算法是一个不错的选择。
// 创建具有所有默认设置的编码器
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
清单 4:SCryptPasswordEncoder
该SCryptPasswordEncoder实现使用scrypt算法对密码进行哈希处理。为了克服自定义硬件scrypt上的密码破解问题,这是一种故意缓慢的算法,需要大量内存。与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
// 创建具有所有默认设置的编码器
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
注册两个表:
create table users
(
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(100) not null,
`password` varchar(100) not null,
`enabled` boolean not null,
PRIMARY KEY (`id`),
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
create table authorities
(
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) unsigned NOT NULL,
`authority` varchar(50) not null,
PRIMARY KEY (`id`),
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
让我们使用 BCryptPasswordEncoder 加密 注册一个用户,用户名:liujinshuai;密码:123456
@RequestMapping("/insert")
public void inserts(){
Users user = new Users();
user.setUsername("liujinshuai");
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("123456");
user.setPassword(result);
user.setEnabled(true);
user1Mapper.inserts(user);
}
执行数据库查询可以发现密码是一个长而加密的数字。在登录时配置加密方法时,它将自动与数据库进行比较,我们将在后面讨论。
设置用户权限:
insert into authorities(id,user_id,authority) value (1,4,"ROLE_USER");
设置权限时,建议使用ROLE_XXX,这是一个规范。这里的user_id对应于users表的id值。该值可以根据您自己的规则生成。我们在这里很方便,只需使用自增ID,就可以看到在这里,我们设置了一个具有ROLE_USER权限的角色。
如果你还不熟悉 SpringBoot 和 MyBatis 的使用方式,请看这里:SpringBoot + MyBatis
4:更改 Security 数据源
在配置的第二部分中,我们使用内存来存储用户帐户和密码。接下来,我们更改了数据源以从数据库中获取用户信息。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home","/insert").permitAll()
.antMatchers("/color")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource);
}
}
(@Override在配置程序中使用an 方法),AuthenticationManagerBuilder则仅用于构建“本地” AuthenticationManager,它是全局变量的子级。在Spring Boot应用程序中,您可以@Autowired将全局的一个转换为另一个Bean,但是除非您自己显式公开它,否则不能对本地的Bean进行转换。
Spring Boot提供了一个默认的全局AuthenticationManager(只有一个用户),除非您通过提供自己的type Bean来抢占它AuthenticationManager。缺省值本身具有足够的安全性,除非您积极需要自定义global,否则不必担心太多AuthenticationManager。如果进行任何构建的配置,AuthenticationManager通常可以在本地对要保护的资源进行配置,而不必担心全局默认值。
不幸的是,这并不全面。因为每个人创建表的方式略有不同,所以我们应该告诉Spring Security如何获取用户和角色信息。另外两个方法需要调用,它们需要包含SQL语句。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home","/insert").permitAll()
.antMatchers("/color")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"SELECT username, password, enabled from users where username = ?")
.authoritiesByUsernameQuery(
"SELECT u.username, a.authority FROM authorities a, users u WHERE u.username = ? AND u.id = a.user_id"
);
}
}
不用担心,您现在无法登录。还记得我们数据库中存储的加密密码吗?我们需要对输入的密码执行相同的加密,请继续阅读。
添加:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
并使用它:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home","/insert").permitAll()
.antMatchers("/color")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.passwordEncoder(passwordEncoder())
.usersByUsernameQuery(
"SELECT username, password, enabled from users where username = ?")
.authoritiesByUsernameQuery(
"SELECT u.username, a.authority FROM authorities a, users u WHERE u.username = ? AND u.id = a.user_id"
);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
接下来让我们再次登录
输入账户:liujinshuai
输入密码:123456
真的很棒,我们成功地从数据中获取了数据。 🍻
5:设置访问权限
表达式根对象的基类是SecurityExpressionRoot。这提供了Web和方法安全性中都可用的一些常用表达式。
清单 5:常见的内置表达式
方法 | 描述 |
---|---|
hasRole(String role) | 返回true当前委托人是否具有指定角色。例如, hasRole(‘admin’)默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。可以通过修改defaultRolePrefixon来自定义DefaultWebSecurityExpressionHandler。 |
hasAnyRole(String… roles) | 返回true当前委托人是否具有提供的任何角色(以逗号分隔的字符串列表形式)。例如, hasAnyRole(‘admin’, ‘user’)默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。可以通过修改defaultRolePrefixon来自定义DefaultWebSecurityExpressionHandler。 |
hasAuthority(String authority) | true如果当前主体具有指定的权限,则返回。例如, hasAuthority(‘read’) |
hasAnyAuthority(String… authorities) | 返回true当前委托人是否具有任何提供的授权(以逗号分隔的字符串列表形式)例如, hasAnyAuthority(‘read’, ‘write’) |
principal | 允许直接访问代表当前用户的主体对象 |
authentication | 允许直接访问Authentication从SecurityContext |
permitAll | 总是评估为 true |
denyAll | 总是评估为 false |
isAnonymous() | 返回true当前委托人是否为匿名用户 |
isRememberMe() | 返回true当前主体是否是“记住我”的用户 |
isAuthenticated() | true如果用户不是匿名的,则返回 |
isFullyAuthenticated() | 返回true如果用户不是匿名或记得,我的用户 |
hasPermission(Object target, Object permission) | 返回true用户是否可以访问给定权限的给定目标。例如,hasPermission(domainObject, ‘read’) |
hasPermission(Object targetId, String targetType, Object permission) | 返回true用户是否可以访问给定权限的给定目标。例如,hasPermission(1, ‘com.example.domain.Message’, ‘read’) |
使用hasRole(字符串角色)来设置只有管理员(ADMIN)有权访问的地址。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home","/insert").permitAll()
.antMatchers("/color").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
可以发现,我们在第三行上将 “ /color” 路径设置为仅管理员(ADMIN)有权访问的路径。
可以发现我们的访问请求被禁止。
这还不够友好,我们应该提醒他们,而不是一串看不懂的字母。
还记得如何在SpringBoot中设置403访问页面吗?非常简单,只需在“ / resources / static / error”目录中创建一个403.html文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1 style="text-align:center">抱歉!你没有访问权限!</h1>
</body>
</html>
让我们再次登录:
成功了!🍻🍻🍻
6:访问当前认证的用户
如果我想获取有关当前主要用户的信息怎么办?
如果您希望获取有关已认证主体的信息,可以通过访问来获得SecurityContextHolder。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
1:获取主体用户名称
String username = authentication.getName();
2:获取主体用户详细信息
Object principal = authentication.getPrincipal();
3:获取主体用户权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority grantedAuthority : authorities) {
if ("ROLE_ADMIN".equals(grantedAuthority.getAuthority())) return "ROLE_ADMIN";
}
默认情况下,会SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着SecurityContext即使SecurityContext未将显式地传递给这些方法的参数,该方法也始终可用于同一执行线程中的方法。ThreadLocal如果在处理了当前委托人的请求之后要清除线程,则以这种方式使用a 是非常安全的。Spring Security的FilterChainProxy确保SecurityContext始终清除。
由于某些应用程序使用ThreadLocal线程的特定方式,因此它们并不完全适合使用。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。 SecurityContextHolder可以在启动时配置策略,以指定希望如何存储上下文。对于独立应用程序,您将使用该SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望让安全线程产生的线程也采用相同的安全身份。这是通过使用实现的SecurityContextHolder.MODE_INHERITABLETHREADLOCAL。您可以通过SecurityContextHolder.MODE_THREADLOCAL两种方式从默认更改模式。第一个是设置系统属性,第二个是在上调用静态方法SecurityContextHolder。
ERROR 1:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”
这是密码编码器错误,需要设置密码存储格式。
方式一:添加{noop}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER");
}
方式二:
@Bean
public UserDetailsService userDetailsService() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("USER", "ADMIN").build());
return manager;
}
ERROR 2:Cannot resolve ‘param’
把 xmlns:th = "http://www.thymeleaf.org"
修改成 xmlns:th ="http://thymeleaf.org"