文章目录
SpringSecurity的基本使用
重启项目后务必要刷新页面
初始化项目
新建springboot项目,选择springsecurity、web、mysql、mybatis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
配置数据库(数据库不存在需手动创建)
spring.datasource.password=111111
spring.datasource.username=root
spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
启动项目
![image-20220102204246245](https://i-blog.csdnimg.cn/blog_migrate/e33339ec03124e80e7d9a504c40c749b.png)
看到他,说明就没问题
默认用户名:user
默认密码:
当然现在登录成功是404,因为我们什么都没有写。
为什么我们会访问8080就是这个页面?
因为security默认是会要求所有请求认证。在我们没有被认证(即登录)时,任何请求默认都会重定向到这个登录页。
![image-20220102205713041](https://i-blog.csdnimg.cn/blog_migrate/4f37eeaeae27f90d15affd3680c6c4a6.png)
编写一个接口,并访问
@RestController
public class TestController {
@GetMapping("/hello")
public String hello() {
return "hello world!";
}
}
访问:localhost:8080/hello
![image-20220102210108554](https://i-blog.csdnimg.cn/blog_migrate/bb2068b2189e6efc270e324c0aafd61e.png)
依然要先登录,然后才能看到
学习配置security
1.自定义登录
前后端分离的话,可以看看这篇文章:https://blog.csdn.net/qq_42682745/article/details/121326021
我们的配置类需要继承WebSecurityConfigurerAdapter
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin() //开启表单登录
.loginPage("/login.html") //定义登录页面
.loginProcessingUrl("/mylogin") //自定义登录请求,form表单中的action就是它
.usernameParameter("myName") //自定义用户名参数,默认表单中必须是username
.passwordParameter("myPass"); //自定义密码参数,默认表单中必须是password
http.authorizeRequests() //认证相关
.antMatchers("/login.html").permitAll() //放行登录页面
.antMatchers("/Main.html").denyAll() //拒绝访问
.anyRequest().authenticated(); //其余请求都必须认证后才能访问
//从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。
// CSRF 为了保证不是其他第三方网站访问,
// 要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果token 和 服务端的 token 匹配成功,则正常访问。
//所以关闭csrf请求才能成功
http.csrf().disable();
}
}
- .antMatchers("/login.html").permitAll() 将matchers中的资源放行,不认证即可访问
- .anyRequest().authenticated() 除去上面matchers中的资源,其余资源访问都需要认证
随便写一个登录页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/mylogin" method="post">
<input type="text" name="myName"/>
<input type="password" name="myPass"/>
<button type="submit">登录</button>
</form>
</body>
</html>
![image-20220102215139144](https://i-blog.csdnimg.cn/blog_migrate/7b802f1d898074c092855b03e0a22e4f.png)
ok,
现在我们访问:localhost:8080/hello , 被重定向到我们自定义的登陆页面了
,再输入user和那串密码,
成功
2.登录成功、登录失败处理
successForwardUrl("/success") //登录成功的post处理接口
.failureForwardUrl("/unsuccess") //登录失败的post处理接口
失败:
![image-20220103102921069](https://i-blog.csdnimg.cn/blog_migrate/9b319b7005253676c62dabb527faa1e2.png)
成功:
![image-20220103103030140](https://i-blog.csdnimg.cn/blog_migrate/848a2260572bf814c3ea4fec57633198.png)
3.密码加密
PasswordEncoder核心接口,进入PasswordEncoder接口,鼠标双击类名,Ctrl+h,
可以看到,加密方式很多啊。
security官方推荐的是BCryptPasswordEncoder。
下面我们在测试类中用一下:使用BCryptPasswordEncoder加密123456
![image-20220103112640939](https://i-blog.csdnimg.cn/blog_migrate/7413adf987781253f5e5099eb288422f.png)
下面我们在测试类中用一下:使用NoOpPasswordEncoder加密123456
NoOpPasswordEncoder,就是不加密密码,只推荐测试使用;这个已经被标记为过时、不推荐使用。
4.自定义查询用户
UserDetailsService、UserDetails接口就是核心,我们需要了解。
进入UserDetailsService接口,鼠标双击类名,Ctrl+h,
![image-20220103110031864](https://i-blog.csdnimg.cn/blog_migrate/2775ff20d3b153599d66e0db2b3d441b.png)
我们就了解,内存中查询用户和数据库查询用户
再看看UserDetails接口,
![image-20220103110514052](https://i-blog.csdnimg.cn/blog_migrate/22051a231dddc7526bbcea50e9652a00.png)
它就是security的一个用户单位,我们使用它的实现类User即可。注意,是security的User不是你自己写的。
1.内存中
在刚刚的配置文件中,加上这两个方法(一个创建获取用户的bean,一个创建密码加密器bean):
@Bean
public UserDetailsService users() {
//创建内存UserDetailsManager对象
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//创建用户张三李四
manager.createUser(User.withUsername("张三").password("123").authorities("p1","p2").build());
manager.createUser(User.withUsername("李四").password("123").authorities("p3").build());
return manager;
}
@Bean
public PasswordEncoder encoder() {
return NoOpPasswordEncoder.getInstance();
}
张三,密码123,权限p1,p2
李四,密码123,权限p3
ok,现在我们试试用这两个账号登录
![image-20220103113618558](https://i-blog.csdnimg.cn/blog_migrate/cbf2b02c0adb97177091b52ca37c00ce.png)
登录成功,下面我们再加上权限
http.authorizeRequests() //认证相关 .antMatchers("/login.html").permitAll() //放行登录页面 .antMatchers("/unsuccess").permitAll() //访问hello请求需要有p1权限 .antMatchers("/hello").hasAnyAuthority("p1") .anyRequest().authenticated(); //其余请求都必须认证后才能访问
先使用,张三(p1、p2权限),登录成功后,浏览器访问localhost:8080/hello
![image-20220103115058779](https://i-blog.csdnimg.cn/blog_migrate/a5e1fb105d32bc381115701f89abf9e3.png)
访问成功,因为张三有p1权限。
下面试试李四(p3),登录成功后,浏览器访问localhost:8080/hello
![image-20220103115501030](https://i-blog.csdnimg.cn/blog_migrate/cecf99f55f41f7cb73bb1e2e6d834f63.png)
403,无权访问
2.数据库
就是写一个service实现UserDetailsService,loadUserByUsername方法即可。调用mapper去数据库查
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库
MyUser myUser = userMapper.findByName(username);
if (myUser == null) {
return null;
}
return new User(myUser.getName(),myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(myUser.getAuth()));
}
}
AuthorityUtils.commaSeparatedStringToAuthorityList(“p1,p2”),这个方法会将字符串以逗号分隔为list
这里图方便,我就伪造数据了。。。。用户名是xp,就返回一个User对象,密码123456,权限p1,p2
@Servicepublic class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if ("xp".equals(username)) { return new User(username,"123456", AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2")); } return null; }}
重启,登录xp
![image-20220103125752121](https://i-blog.csdnimg.cn/blog_migrate/f966b58d9124aac8375518286ea42817.png)
登录成功,并且访问/hello也是可以的
5.角色和权限
权限
权限在前文已经涉及到了
.antMatchers("/hello").hasAnyAuthority("p1","p2") //有其中一个权限即可
相信大家很容易理解,有什么权限才能干什么事
角色
其实和权限差不多
角色,必须以ROLE_ 开头,后面跟角色名
@Servicepublic class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if ("xp".equals(username)) { return new User(username,"123456", AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2,ROLE_admin,ROLE_test")); } return null; }}
我们再改一下security的配置,加上:.antMatchers("/test").hasAnyRole(“admin”,“test”)
在配置类中就只需要写角色名,不能加ROLE_
http.authorizeRequests() //认证相关
.antMatchers("/login.html").permitAll() //放行登录页面
.antMatchers("/unsuccess").permitAll()
.antMatchers("/Main.html").denyAll() //拒绝访问
//访问hello请求需要有p1权限
.antMatchers("/hello").hasAnyAuthority("p1","p2")
.antMatchers("/test").hasAnyRole("admin","test")
.anyRequest().authenticated(); //其余请求都必须认证后才能访问
访问/test接口需要有角色admin或者test,有其中一个角色即可访问。登录xp,访问test
IP控制
//指定ip才能访问
.antMatchers("/test").hasIpAddress("192.168.1.4")
localhost的ip是 0:0:0:0:0:0:0:1
127.0.0.1 的ip就是本身
自定义403权限不足提示
https://www.bilibili.com/video/BV1gb4y1b7XE?p=20
自己写一个类,实现AccessDeniedHandler接口
![image-20220103154338258](https://i-blog.csdnimg.cn/blog_migrate/255051d8ca415a4692e6c9699ddc09d5.png)
将MyAccessDeniedHandler注入到配置类,然后
6.注解
@Secured
作用于类、方法
验证是否有具有指定角色,有才能访问
角色必须是以ROLE_ 开头的
现在,将配置类的角色相关的注释掉
1.启动类,增加@EnableGlobalMethodSecurity(securedEnabled = true)
2.在test接口上加上
@Secured("ROLE_admin,ROLE_test")@GetMapping("/test")public String test() { return "您可以访问/test接口";}
重启,登录访问test,成功。
现在将test接口的角色,改一下
@Secured("ROLE_xx")@GetMapping("/test")public String test() { return "您可以访问/test接口";}
![](https://i-blog.csdnimg.cn/blog_migrate/b02324c5a05b15a8c75f12265831f628.png)
无权访问
值得一提的是,@Secured(“ROLE_admin,ROLE_test”)和配置中的hasAnyRole(“admin”,“test”)。虽然都能写多个角色。hasAnyRole(“admin”,“test”)只需要其中一个权限就能访问。但是@Secured(“ROLE_admin,ROLE_test”)必须全部具备才能访问
@PreAuthorize()和@PostAuthorize()
它两也作用于方法或者类。
判断有不有权限
@PreAuthorize(),执行方法前判断
@PostAuthorize(),执行方法后判断
很明显,我们既然做权限控制,自然是使用@PreAuthorize()更多,它两的用法一样。
1.启动类注解:
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
2.修改test接口
//@Secured("ROLE_admin,ROLE_test")
@PreAuthorize("hasAnyAuthority('p1','p2')")
@GetMapping("/test")
public String test() {
return "您可以访问/test接口";
}
3.将配置类中权限控制相关的注释调
启动–登录–访问test–成功
修改test接口:
@PreAuthorize("hasAnyAuthority('p8')")
@GetMapping("/test")
public String test() {
return "您可以访问/test接口";
}
![image-20220103142045366](https://i-blog.csdnimg.cn/blog_migrate/42629f5eb9bf8ea9b5bef772b4c2b0f7.png)
@PreAuthorize里面的写法和配置文件一样,接口都是有提示的
7.记住我
我们需要在配置类中加上一些东西:
1.注入依赖:
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private DataSource dataSource; //配置好mysql参数后,springboot会自动装配一个DataSource
@Autowired
private PersistentTokenRepository persistentTokenRepository;
2.写bean方法:
@Bean
//返回持久化token到数据库的bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//配置数据源
jdbcTokenRepository.setDataSource(dataSource);
//自动创建所需要的表,第二次启动需要注释掉不然报错
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
3.加上配置
http.rememberMe() //开启记住我功能
.rememberMeParameter("remember") //自定义记住我参数名,remember-me
.userDetailsService(myUserDetailsService) //指定查询用户的service对象
.tokenRepository(persistentTokenRepository); //指定token持久化对象
4.修改test接口,权限改回来
@PreAuthorize("hasAnyAuthority('p1')")
@GetMapping("/test")
public String test() {
return "您可以访问/test接口";
}
5.登陆页面修改
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/mylogin" method="post">
<input type="text" name="myName"/><br/>
<input type="password" name="myPass"/></br/>
记住我:<input type="checkbox" name="remember" checked="checked">
<button type="submit">登录</button>
</form>
</body>
</html>
重启,访问:
第二次启动记得将建表的代码注释掉
现在,我们关闭浏览器。直接访问:localhost:8080/test,看会不会让我们再登录。
直接访问到了,不会跳转到登录页面
保存到数据库的token有效期默认为两周,
我们也可以自己设置
.tokenValiditySeconds(10086) //设置token有效时间,单位秒
完整的配置类代码:
import javax.sql.DataSource;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder encoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
//返回持久化token到数据库的bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动创建所需要的表,第二次启动需要注释掉不然报错
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin() //开启表单登录
.loginPage("/login.html") //定义登录页面
.loginProcessingUrl("/mylogin") //自定义登录请求,form表单中的action就是它
.usernameParameter("myName") //自定义用户名参数,默认表单中必须是username
.passwordParameter("myPass") //自定义密码参数,默认表单中必须是password
.successForwardUrl("/success")
.failureForwardUrl("/unsuccess");
http.authorizeRequests() //认证相关
.antMatchers("/login.html").permitAll() //放行登录页面
.antMatchers("/unsuccess").permitAll()
.antMatchers("/Main.html").denyAll() //拒绝访问
//访问hello请求需要有p1权限
.antMatchers("/hello").hasAnyAuthority("p1","p2")
//.antMatchers("/test").hasAnyRole("admin","test")
.anyRequest().authenticated(); //其余请求都必须认证后才能访问
http.rememberMe() //开启记住我功能
.tokenValiditySeconds(10086) //设置token有效时间,单位秒
.rememberMeParameter("remember") //自定义记住我参数名,remember-me
.userDetailsService(myUserDetailsService) //指定查询用户的service
.tokenRepository(persistentTokenRepository()); //token持久化
//从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。
// CSRF 为了保证不是其他第三方网站访问,
// 要求访问时携带参数名为_csrf 值为 token(token 在服务端产生)的内容,如果token 和服务端的 token 匹配成功,则正常访问。
//所以关闭csrf请求才能成功
http.csrf().disable();
}
}
8.退出登录
退出就很简单了,我们找个页面,写个a标签都可以,security默认的退出就是"/logout"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功<a href="/logout">退出</a>
</body>
</html>
![image-20220103211018764](https://i-blog.csdnimg.cn/blog_migrate/aca983894b28ba2d8a0aacd01010e776.png)
如果你想自定义退出路径也可以:
即可。