用户认证(设置用户名、密码)
设置登录的用户名和密码
- 第一种方式:通过配置文件
修改application.properties,添加如下内容:
spring.security.user.name=antherd
spring.security.user.password=antherd
- 第二种方式:配置类
package com.antherd.securitydemo.config;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
}
// PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
- 第三种方式:自定义编写实现类
- 创建配置类,设置使用哪个userDetailService实现类
package com.antherd.securitydemo.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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
- 编写实现类,返回User对象,User对象有用户名密码和操作权限
package com.antherd.securitydemo.service;
import com.antherd.securitydemo.entity.Users;
import com.antherd.securitydemo.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
return new User("lucy", new BCryptPasswordEncoder().encode(123), auths);
}
}
启动项目,访问:http://localhost:8111/test/hello,自动跳转到登陆页面,输入配置的用户名密码后,跳转到访问页面。
用户认证(查询数据库完成认证)
整合MyBatisPlus完成数据库操作
- 引入相关依赖
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok 用来简化实体类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 创建数据库demo和数据库表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
添加数据
INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (1, 'lucy', '123');
INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (2, 'mary', '456');
- 创建users表对应实体类
package com.antherd.securitydemo.entity;
import lombok.Data;
@Data
public class Users {
private Integer id;
private String username;
private String password;
}
- 整合MybatisPlus,创建接口,继承map的接口
package com.antherd.securitydemo.mapper;
import com.antherd.securitydemo.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
- 在UserDetailsService调用mapper里面的方法查询数据库进行用户认证
package com.antherd.securitydemo.service;
import com.antherd.securitydemo.entity.Users;
import com.antherd.securitydemo.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper<>();
// where username = ?
wrapper.eq("username", username);
Users users = usersMapper.selectOne(wrapper);
// 判断
if (users == null) { // 数据库没有用户名, 认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}
- 在启动类添加注解MapperScan
@MapperScan("com.antherd.securitydemo.mapper")
- 配置文件中配置数据库信息
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
- 启动测试
用户认证(自定义用户登录页面)
- 在配置类实现相关的配置
package com.antherd.securitydemo.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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 自定义自己编写的登录页面
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功后,跳转路径
.and().authorizeRequests() //
.antMatchers("/", "/test/hello", "/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护
}
}
- 创建相关页面,controller
在resource文件夹下创建static文件夹,添加文件login.html,用户名、密码参数必须为:username、password
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br/>
密码:<input type="text" name="password">
<br/>
<input type="submit" value="login" />
</form>
</body>
</html>
在TestController中添加接口
@GetMapping("/index")
public String index() {
return "hello index";
}
- 启动测试
访问:http://localhost:8111/test/hello 不需要认证
访问:http://localhost:8111/test/index 跳转到自定义认证页面
用户认证(基于权限访问控制)
- ① hasAuthority方法:如果当前的主体有指定的权限,则返回true,否则返回false
- 在配置类设置当前访问地址有哪些权限
// 当前登录用户,只有具有admins权限才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admins")
- 在UserDetailsService,把返回User对象设置权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); // 将admins改成123,返回登录访问403
- ② hasAnyAuthority方法:如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
.antMatchers("/test/index").hasAnyAuthority("admins, manager")
- ③ hasRole方法:如果用户具备给定角色就允许访问,否则出现403。如果当前主体具有指定的角色,则返回true
查看源码
return "hasRole('ROLE_" + role + "')";
MyUserDetailsService
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_sale"); // 角色权限需要加上"ROLE_"前缀
SecurityConfigTest
.antMatchers("/test/index").hasRole("sale")
- ④ hasAnyRole方法:表示用户具备任何一个条件都可以访问
MyUserDetailsService
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
SecurityConifgTest
.antMatchers("/test/index").hasAnyRole("sale")
用户认证(自定义403页面)
MyUserDetailsService
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale1`");
在resource/static中新建页面 unauth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<h1>没有访问权限!</h1>
</body>
</html>
在配置类中配置即可,SecurityConfigTest.configure,添加如下内容:
// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
用户授权(注解使用)
- ① Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀
"ROLE_"
- 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true)
- 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@Secured({"ROLE_sale", "ROLE_manager"})
public String update() {
return "hello update";
}
- userDetailService设置用户角色
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
- ② PreAuthorize:注解适合进入方法前的权限校验,可以将登陆用户的roles/permissions参数传到方法中
- 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
- 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
return "hello update";
}
- userDetailService设置用户角色
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
- ③ PostAuthority:注解使用并不多,在方法执行后再进行权限校验,适合验证带有返回值的权限
使用此注解先要在启动类上开启注解功能
- 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
- 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
return "hello update";
}
- userDetailService设置用户角色,admin,controller中权限为admins
AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_admin, ROLE_sale");
- 访问:http://localhost:8111/test/update ,返回403页面,但是方法执行了
- ④ PreFilter:传入方法数据进行过滤
- ⑤ PostFilter:方法返回数据进行过滤
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
@GetMapping("/getAll")
@PostAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.username=='admin1'")
public List<Users> getAllUser() {
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(11, "admin1", "6666"));
list.add(new Users(21, "admin2", "8888"));
System.out.println(list);
return list;
}
访问:http://localhost:8111/test/getAll ,看一下返回值
用户注销
- 在配置类中添加退出的配置
// 退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
- 测试:
① 修改配置类,登录成功之后跳转到成功页面
.defaultSuccessUrl("/success.html").permitAll() // 登录成功后,跳转路径
② 在成功页面添加超链接,写设置的退出路径
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!
<a href="/logout">退出</a>
</body>
</html>
③ 登录成功后,在点击页面点击退出
访问:http://localhost:8111/login.html、然后新开标签页访问 http://localhost:8111/login.html,可以访问,在点击退出按钮,在访问 http://localhost:8111/login.html,发现不能访问
自动登录
安全框架机制实现自动登录
一、实现原理
二、具体实现
- 创建数据库表(可以自动生成)
JdbcTokenRepositoryImpl.CREATE_TABLE_SQL
CREATE TABLE persistent_logins (
username VARCHAR ( 64 ) NOT NULL,
series VARCHAR ( 64 ) PRIMARY KEY,
token VARCHAR ( 64 ) NOT NULL,
last_used TIMESTAMP NOT NULL)
- 配置类,注入数据源,配置操作数据库对象
// 注入数据源
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// jdbcTokenRepository.setCreateTableOnStartup(true); // 自动创建表
return jdbcTokenRepository;
}
- 配置类中配置自动登录
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置有效时长,单位秒
.userDetailsService(userDetailsService)
- 在登录页面中添加复选框
<input type="checkbox" name="remember-me" />自动登录
<br />
- 访问 http://localhost:8111/login.html 浏览器中存入了cookies,并且数据库中记录了相关信息
关闭浏览器后,重启打开,访问 http://localhost:8111/login.html 发现登录任然有效
CSRF
跨站请求伪造(英语:Cross-site request forgery)
从Spring Security 4.0 开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用,Spring Security CSRF会针对PATCH、POST、PUT 和 DELETE 方法进行防护
在pom文件中引入如下依赖
<!-- 对Thymeleaf添加Spring Security标签支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在templates目录下新建三个页面
csrf/csrf_token.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户修改</title>
</head>
<body>
<div>
<span th:text="${_csrf.token}"></span>
</div>
</body>
</html>
csrf/csrfTest.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户修改</title>
</head>
<body>
<div align="center">
<form method="post" action="update_token">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
用户名:<input type="text" name="username" /> <br />
密 码:<input type="password" name="password" /> <br />
<button type="submit">修改</button>
</form>
</div>
</body>
</html>
login/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br/>
密码:<input type="text" name="password">
<br/>
<input type="checkbox" name="remember-me" />自动登录
<br />
<input type="submit" value="login" />
</form>
</body>
</html>
新建 CSRFUserDetailsService
package com.antherd.securitydemo.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service("userDetailsService")
public class CSRFUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<SimpleGrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority("role"));
UserDetails userDetails = new User("lucy", new BCryptPasswordEncoder().encode("123"),
list);
return userDetails;
}
}
SecurityConfigCsrf
package com.antherd.securitydemo.config;
import org.springframework.beans.factory.annotation.Autowired;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfigCsrf extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// 实现用户身份认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置url的访问权限
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/**update**").permitAll()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated();
// 关闭csrf保护功能
// http.csrf().disable();
// 使用自定义的登录窗口
http.formLogin()
.loginPage("/userLogin").permitAll()
.usernameParameter("username").passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
}
}
CSRFController
package com.antherd.securitydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class CSRFController {
@GetMapping("/toupdate")
public String test(Model model) {
return "csrf/csrfTest";
}
@PostMapping("/update_token")
public String getToken() {
return "csrf/csrf_token";
}
}
LoginController
package com.antherd.securitydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/userLogin")
public String login() {
return "login/login";
}
}
现将 csrfTest页面中如下内容注释掉
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
启动测试,访问 http://localhost:8111/toupdate , 发现登录触发CSRF,跳转到 /userLogin?error 页面
取消上面注释后,在启动测试,可以正常登录