1. 认证
1.1 方式一
通过配置文件进行设置认证
application.properties
spring.security.user.name=root
spring.security.user.password=123456
1.2 方式二
通过配置类进行设置认证
在config文件夹下创建一个securityConfig.java
SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
1.3 方式三
通过自定义编写实现类进行设置认证
第一步创建配置类,设置使用哪个userDetailsService实现类
SecurityConfig2.java
@Configuration
public class SecurityConfig2 extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
第二步编写实现类,返回User对象,User对象有用户名密码和操作权限
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("roles");
return new User("qwe123",new BCryptPasswordEncoder().encode("123456"),roles);
}
}
authorities不允许为Null
1.4 实现数据库来认证用户登录
1.4.1 准备数据库
CREATE TABLE users(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20) UNIQUE NOT NULL,
PASSWORD VARCHAR(100)
);
INSERT INTO users VALUES(1,'张san','123');
INSERT INTO users VALUES(2,'李si','456');
1.4.2 搭建项目
1.4.3 导入依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</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>
1.4.4 编写实体类
User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
1.4.5 编写Mapper文件
UsersMapper.java
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
1.4.6 编写配置文件
application.properties
# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
1.4.7 开启Mapper扫描
Springboot05SpringSecurityApplication.java
@SpringBootApplication
@MapperScan("com.test.mapper")
public class Springboot05SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot05SpringSecurityApplication.class, args);
}
}
1.4.8 编写security配置文件
SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
1.4.9 编写实现类
UserDetailServiceImpl.java
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("roles");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
1.4.10 编写controller层
TestController.java
@RestController
public class TestController {
@RequestMapping("/test")
public String test() {
return "ok";
}
}
1.4.11 启动启动类进行测试
在浏览器输入:http://localhost:8080/test
- 输入错误的账号和密码
- 输入正确的账号和密码
1.5 自定义登录页面
1.5.1 导入依赖
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1.5.2 自定义页面配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//自定义登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//自定义登录页面
.loginPage("/login.html")//登录页面设置
.loginProcessingUrl("/user/login")//登录页面的访问路径
.defaultSuccessUrl("/test").permitAll()//登录成功的访问路径
.and().authorizeRequests()
.antMatchers("/","/test","/user/login").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();//关闭csrf防护
}
}
1.5.3 编写login.html页面
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
注意:页面提交方式必须为 post 请求,所以上面的页面不能使用,用户名,密码必须为username,password
原因:
在执行登录的时候会走一个过滤器 UsernamePasswordAuthenticationFilter
如果修改配置可以调用 usernameParameter()和 passwordParameter()方法。
<form action="/login"method="post">
用户名:<input type="text"name="loginAcct"/><br/>
密码:<input type="password"name="userPswd"/><br/>
<input type="submit"value="提交"/>
</form>
1.5.4 启动启动类进行测试
在浏览器输入:http://localhost:8080/index
2. 授权
2.1 基于权限进行访问控制
2.1.2 hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true,否则返回 false
- 修改配置类
- 测试:
- 在UserDetailService中的将限修改为admins
- 测试:
- 缺点:对多个权限不能设置同一个路径
2.1.2 hasAnyAuthority 方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true.
- 修改配置类
- 在UserDetailService中的将限修改为admins或两个都可以,只要其中一个权限就可以访问
- 测试:
2.2 基于角色进行访问控制
2.2.1 hasRole 方法
如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true
底层源码:
- 修改配置
查看源码可知,我们给用户赋角色时记得在前面加ROLE_
- 测试:
2.2.2 hasAnyRole方法
表示用户具备任何一个条件都可以访问。
- 修改配置类
- 给用户添加角色:只要其中一个角色就可以访问
2.3 自定义403页面
默认403页面
- 修改配置类
- 添加控制器
@RequestMapping("/to403")
public String to403() {
return "view/403";
}
- 添加403.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1>403页面</h1>
</body>
</html>
- 测试
2.4 注解使用
2.4.1 @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
- 使用注解先要开启注解功能!
@SpringBootApplication
@MapperScan("com.test.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Springboot05SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot05SpringSecurityApplication.class, args);
}
}
- 在控制器方法上添加注解
@RequestMapping("/update")
@ResponseBody
@Secured({"ROLE_user","ROLE_menu"})
public String update() {
return "hello update";
}
- 添加用户角色
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.
commaSeparatedStringToAuthorityList("admins,manager,ROLE_user,ROLE_menu");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
- 测试
2.4.2 @PreAuthorize
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
- 先开启注解功能:
@SpringBootApplication
@MapperScan("com.test.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class Springboot05SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot05SpringSecurityApplication.class, args);
}
}
- 在控制器方法上添加注解
@RequestMapping("/update")
@ResponseBody
//@Secured({"ROLE_user","ROLE_menu"})
@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
return "hello update";
}
- 添加用户角色
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.
commaSeparatedStringToAuthorityList("admins,manager,ROLE_user,ROLE_menu");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
- 测试
2.4.3 @PostAuthorize
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.
- 先开启注解功能
@SpringBootApplication
@MapperScan("com.test.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class Springboot05SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot05SpringSecurityApplication.class, args);
}
}
- 在控制器方法上添加注解
@RequestMapping("/update")
@ResponseBody
//@Secured({"ROLE_user","ROLE_menu"})
//@PreAuthorize("hasAnyAuthority('admins')")
@PostAuthorize("hasAnyAuthority('admins')")
public String update() {
return "hello update";
}
- 添加用户角色
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.
commaSeparatedStringToAuthorityList("admins,manager,ROLE_user,ROLE_menu");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
- 测试
2.4.4 @PostFilter
@PostFilter :权限验证之后对数据进行过滤
留下用户名是 admin1 的数据表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
- 在控制器方法上添加注解
@RequestMapping("getAll")
@PostAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<Users> getAllUser(){
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(1,"admin1","6666"));
list.add(new Users(2,"admin2","888"));
return list;
}
- 添加用户角色
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.
commaSeparatedStringToAuthorityList("admins,manager,ROLE_user,ROLE_menu");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
- 测试
2.4.5 @PreFilter
@PreFilter: 进入控制器之前对数据(传入的参数)进行过滤
- 在控制器方法上添加注解
@RequestMapping("getTestPreFilter")
@PostAuthorize("hasAnyAuthority('admins')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<Users> getTestPreFilter(@RequestBody List<Users>
list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}
- 添加用户角色
@Service("userDetailsService")
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("认证失败!");
}
List<GrantedAuthority> roles = AuthorityUtils.
commaSeparatedStringToAuthorityList("admins,manager,ROLE_user,ROLE_menu");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),roles);
}
}
- 测试
测试的 Json 数据:
[{
"id": "1",
"username": "admin",
"password": "666"
},{
"id": "2",
"username": "admins",
"password": "888"
},{
"id": "3",
"username": "admins11",
"password": "11888"
},{
"id": "4",
"username": "admins22",
"password": "22888"
}]
3. 用户注销
3.1在配置类中配置
//自定义登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().logoutUrl("/loginOut").logoutSuccessUrl("/login").permitAll();
//自定义403
http.exceptionHandling().accessDeniedPage("/to403");
http.formLogin()//自定义登录页面
.loginPage("/login")//登录页面设置
.loginProcessingUrl("/user/login")//登录页面的访问路径
.defaultSuccessUrl("/success").permitAll()//登录成功的访问路径
.and().authorizeRequests()
.antMatchers("/","/test","/user/login","/login").permitAll()
//当前登录用户带有admins权限
//.antMatchers("/index").hasAuthority("admins")
//.antMatchers("/index").hasAnyAuthority("admins,manager")
//.antMatchers("/index").hasRole("user")
//.antMatchers("/index").hasAnyRole("user,menu")
.anyRequest().authenticated()
.and().csrf().disable();//关闭csrf防护
}
3.2 编写controller
@RequestMapping("/success")
public String success() {
return "view/success";
}
3.3 编写success.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功 <a href="/loginOut">注销</a>
</body>
</html>
4. 自动登录
4.1 自动登录实现的方式有
- cookie技术
- 安全框架机制实现
4.2 原理分析
4.3 功能实现
4.3.1 编写配置文件
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);//系统内部为我们创建表,第一次执行会创建,以后要执行就要删除掉!
return tokenRepository;
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
//记住我
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//设置有效时长,单位为秒
.userDetailsService(userDetailsService);
}
}
4.3.2 编写前端
name为固定值:remember-me
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
4.3.3 测试
登录后按F12可以看到存储remember-me这个cookie
数据库自动存储数据
5. CSRF
5.1 什么是CSRF
CSRF:跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
5.2 了解原理
- 生成 csrfToken 保存到 HttpSession 或者 Cookie 中。
- SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository
- 当前接口实现类:HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository
- 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter 过滤器来完成。
5.3 简单案例
5.3.1导入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
5.3.2 编写配置文件
这句代码是关闭安全配置 csrf,因为默认是开启的,所以测试是需要注释掉,开启。
//http.csrf().disable();//关闭csrf防护
5.3. 3 在登录页面添加隐藏域
没加这句代码即使登录成功也会跳转到失败的页面
<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>
内容参考:https://www.bilibili.com/video/BV15a411A7kP
仅用于学习!