1.什么是SpringSecurity,能做什么?
它是一个安全框架,主要能帮助我们做认证(你是谁)和授权(你能做什么)两大功能。
2.Springboot集成SpringSecurity(体验版)
1.首先导入依赖
<!--pringboot依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.编写一个控制器。写一个映射方法。前端访问这个URL。你会发现会被Security拦截,需要输入用户名和密码。用户名security默认是user。密码默认是程序控制台上打印的。(Using generated security password: xxxxxxxx)
@Controller
public class HelloSecurityController {
/**
* 第一次测试security。
* @return
*/
@GetMapping("/helloSecurity")
@ResponseBody
public String helloSecurity(){
return "helloSecurity";
}
}
3. 如果想要打破默认的security的用户名和密码(自定义登录与密码),只需在yaml配置如下:
spring:
security:
user:
name: admin
password: admin
3.基于内存版本的登录验证与角色权限管理
假设现在有一个需求是这样的:给你一个部门管理系统。
系统有两个角色:USER,ADMIN。
有两个功能:
1)查看部门用户列表 (USER,ADMIN角色都可以使用)
2)删除部门某用户(ADMIN角色才能使用)
1. 编写一个类继承WebSecurityConfigurerAdapter。重写configura方法。在方法中加入用户名和密码。并且在配置类上开启Security功能(@EnableWebSecurity)和
@EnableGlobalMethodSecurity(prePostEnabled = true)
注意:security5版本以上,需要密码加密。不然会出现一个passwordEncoder null 的异常.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456")).roles("ADMIN");
auth.inMemoryAuthentication().withUser("zs").password(passwordEncoder.encode("123456")).roles("USER");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
其次,在controller中加入这两个功能代码如下:
/**
* 展示部门列表功能
* @return
*/
@GetMapping("/show/userList")
@PreAuthorize(value = "hasAnyRole('USER','ADMIN')")
@ResponseBody
public String showUserList(){
return "展示部门用户列表(角色要求:USER/ADMIN)";
}
/**
* 删除某部门用户功能
* @return
*/
@GetMapping("/deleteUser")
@PreAuthorize(value = "hasAnyRole('ADMIN')")
@ResponseBody
public String deleteUser(){
return "删除某用户(角色要求:ADMIN)";
}
2. 其次在用浏览器测试,你会发现zs,admin 都可以使用展示部门列表,但是删除功能只能是admin用户才可以。
注意:浏览器在测试的时候没换一个账户时,需要清除浏览器缓存在测试。 如果出现403错误,就表示被拒绝了,因为403错误页面是Springboot自带的。
3.最佳实战:基于数据库版本
前面展示了基于内存版本的,现在升级为基于数据库版本的.
现在想要实现的是:项目首先进入登录页面,登录部门管理系统。登录成功后,部门管理系统会根据不同的角色控制他们的权限功能。
补充:SpringSecurity框架中的UserDetailsService 和 User 类
UserDetails: 是一个接口,用来获取用户权限、密码、账户、判断账户是否锁定等操作。
user:是一个SpringSecurity高度抽象的用户类,实现了UserDetails接口。
所以如果你想基于数据库版本的。就必须实现UserDetailsService接口,重写里面的
public UserDetails loadUserByUsername(String username); 根据username去数据库登录登录校验查询。然后构建一个UserDetails 返回给security框架。
准备工作:
准备sys_user表,sys_role表,sys_user_role_relation表。
sys_user:
+-----------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| realName | varchar(255) | YES | | NULL | |
| username | varchar(255) | YES | | NULL | |
| password | varchar(255) | YES | | NULL | |
| accountNonExpired | tinyint(1) | YES | | 1 | |
| accountNonLocked | tinyint(1) | YES | | 1 | |
| credentialsNonExpired | tinyint(1) | YES | | 1 | |
| enabled | tinyint(1) | YES | | 1 | |
| createTime | date | YES | | NULL | |
+-----------------------+--------------+------+-----+---------+----------------+
sys_role:
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| role | varchar(255) | YES | | NULL | |
| describe | varchar(255) | YES | | NULL | |
+----------+--------------+------+-----+---------+----------------+
sys_user_role_relation:
+--------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------+------+-----+---------+-------+
| userId | bigint(20) | NO | | NULL | |
| roleId | bigint(20) | NO | | NULL | |
+--------+------------+------+-----+---------+-------+
Dao层框架自定义,本次使用mybatisPlus。用mybatisplus自动生成对应的dao层代码。所以dao层代码全部省略了。
加入themeleaf依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
前端页面(随便写一下):
登录页面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Security Example </title>
</head>
<body>
<!--显示失败信息-->
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login.do}" 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>
系统主页:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<a href="/show/userList">查看用户</a><br>
<a href="/deleteUser">删除用户</a><br>
</body>
</html>
查看用户列表页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>展示用户</title>
</head>
<body>
<h2>展示所有用户(角色要求:ADMIN/USER)</h2>
</body>
</html>
删除用户页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>删除用户(角色要求:ADMIN)</h2>
</body>
</html>
后端Java:
-
自定义一个实体类MySecurityUser implement UserDetails。重写里面所有的方法。
-
编写一个UserDetailsService的实现类,重写loadUserByUsername方法。在方法中查询数据库构建MySecurityUser对象返回即可。
-
在WebSecurityConfigurerAdapter 这个类中改版不要基于内存的了,而是基于UserDetailsService的。
1.
@Data
public class MySecurityUser implements UserDetails {
private Long id;
private String username;
private String realName;
private String password;
private Integer accountNonExpired;
private Integer accountNonLocked;
private Integer credentialsNonExpired;
private Integer enabled;
private Date createTime;
List<GrantedAuthority> authorityList; // 该用户的角色列表集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired==1;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked==1;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired==1;
}
@Override
public boolean isEnabled() {
return enabled==1;
}
}
2.
/**
* 基于数据库版本的控制角色权限:
* 重点是在loadUserByUsername根据username去数据库查询(相当于一个登录功能)。
* 如果查询到了,就构建UserDetails 对象给security框架。
*/
@Component("MyUserDetailsImpl")
public class MyUserDetailsImpl implements UserDetailsService {
@Autowired
ISys_userService iSys_userService;
@Autowired
ISys_user_role_relationService user_role_relationService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MySecurityUser resUser = null;
// 根据username查询
Sys_user sys_user = iSys_userService.getOne(new QueryWrapper<Sys_user>().eq("username", username));
if(sys_user!=null){
// 构建一个UserDetails对象给框架。
resUser = new MySecurityUser();
BeanUtils.copyProperties(sys_user,resUser);
// 查询该username用户的权限
List<GrantedAuthority> authorities = user_role_relationService.getRoleByUserId(resUser.getId());
resUser.setAuthorityList(authorities);
}
return resUser;
}
}
// 我这里构建username用户权限的代码是如下。重点想表达的意思是:角色名字前面需要加入 ROLE_ 这是security框架规定的。
@Override
public List<GrantedAuthority> getRoleByUserId(Long userId) {
List<Sys_role> list = user_role_relationMapper.getRoleByUserId(userId);
List<GrantedAuthority> collect = list.stream().map((role) -> {
GrantedAuthority g = new SimpleGrantedAuthority("ROLE_" + role.getRole());
return g;
}).collect(Collectors.toList());
return collect;
}
3.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("MyUserDetailsImpl")
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 改版基于userDetailsService 来实现数据库版本。
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 表示哪些请求需要被放行。(首页/登录页/静态资源等允许放行)
.antMatchers("/", "/static/**").permitAll()
//而其他的请求都需要认证
.anyRequest()
.authenticated()
.and()
// 设置自定义的登录页面url
.formLogin()
.loginPage("/index.html")
//指定处理登录请求的路径,对应form表单的action地址
.loginProcessingUrl("/login.do").permitAll()
// 指定错误时跳转到的url
//.failureUrl("/error")
// 指定登录成功跳转的url路径
.defaultSuccessUrl("/main.html",true);
}
}
4. 假设现在数据库中的数据是这样的:
张三(zs),拥有USER角色。
管理员(admin),拥有ADMIN,USER两个角色。
项目测试会发现张三只能使用展示用户列表功能, 删除功能只能是拥有ADMIN角色的管理员才可以使用。
总结:
本次记录自己学习security框架的成果。
版本3其实就是对于基于内存的改本。 加入了dao层的逻辑。
主要就是在
MyUserDetailsImpl类中 自己根据username 查询数据库自己构建一个UserDetails类扔给security框架而已。
如果我自己自己实现这种权限控制,大致思路就是在访问controller的时候,先经过过滤器或者拦截器。然后在此处判断该用户是否有资格访问该映射方法。如果有资格,放行。否者拦截。
后续有时间可以看看security的原理