1.SpringSecurity初体验
SpringBoot为SpringSecurity自动化配置了很多东西,只要把security的依赖导进来,所有的方法都会被security保护,需要登录才可以访问。
默认情况下,在启动SpringBoot项目的时候,会生成一个随机的密码,账户名是user,使用它可以进行登录。
如果使用postman进行访问,可以在Basis Auth中配置账号密码。如果验证成功,会自动跳转到/目录。
此时可能发生的404错误是由于/目录下访问不到资源而导致的404.
1.1 配置用户名密码
如果配置了用户名和密码,SpringBoot项目启动的时候不再生成随机密码。
配置密码的方式有3种:
- 在配置文件中配置
- 在配置类中配置
- 保存到数据库
1.2 在配置文件中配置
1 | spring: |
1.3 在配置类中配置
需要继承WebSecurityConfigurerAdapter
1 |
|
由于 Spring Security 支持多种数据源,例如内存、数据库、LDAP 等,这些不同来源的数据被共同封装成了一个 UserDetailService 接口,任何实现了该接口的对象都可以作为认证数据源。
因此我们还可以通过重写 WebSecurityConfigurerAdapter 中的 userDetailsService 方法来提供一个 UserDetailService 实例进而配置多个用户:
1 |
|
但是需要注意的是,两种方式只能选择其中一种。
1.4 在数据库中配置
从数据库中配置就需要创建数据表保存账户、角色信息。
建表语句:
1 | /* |
user
id | username | password | enabled | locker |
---|---|---|---|---|
1 | root | $2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq | 1 | 0 |
2 | admin | $2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq | 1 | 0 |
3 | sang | $2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq | 1 | 0 |
role
id | name | nameZh |
---|---|---|
1 | ROLE_dba | 数据库管理员 |
2 | ROLE_admin | 系统管理员 |
3 | ROLE_user | 用户 |
user_role
root用户拥有dba,admin角色。
admin用户拥有admin角色。
user用户拥有user角色。
id | uid | rid |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 2 |
4 | 3 | 3 |
创建好表后,我们需要连接数据库,这里我就使用JPA来连接数据库,在连接数据库之前记得导入mysql、druid、jpa的依赖。
我们还需编写配置文件和创建实体User,Role。
配置文件:
1 | spring: |
User类实现了UserDetails
接口,并实现了其方法。UserDetails接口是告诉Spring Security,我这个User类的哪些属性对应着登录名,密码等。
我们希望在验证用户是否在系统中存在的时候,把该用户的角色给它赋值上。这里使用了JPA的导航属性,在查询出User类的时候,就会把对应的角色给赋值了。
1 |
|
Role:
1 |
|
service实现了UserDetailsService,稍后在配置Spring Security的用户时会用到。
1 |
|
UserDao:
1 | public interface UserDao extends JpaRepository<User,Integer> { |
DataBaseSecurityConfiguration:
配置Spring Security的用户信息,httpConfig,密码编码等。
1 |
|
HelloController:
编写3个接口,分别测试dba,admin,user的访问权限。
1 |
|
小结:使用数据库配置Spring Security的账户密码
- 实体类需要实现UserDetail接口,提供给Spring Security一些信息做为验证的依据
- Spring Security在配置用户信息的时候需要一个UserDetailService,我们需要创建一个Service实现UserDetailService接口,查询前端登录的用户在系统中是否存在,其他验证工作交给Spring Security即可
使用JDBC从数据库中配置
在org\springframework\security\spring-security-core\5.4.2\spring-security-core-5.4.2.jar!\org\springframework\security\core\userdetails\jdbc\users.ddl
如果是使用Mysql数据库,把varchar_ignorecase改成varchar就可以了
1 | create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null); |
使用JdbcUserDetailsManager
1 |
|
1.5 多个HttpConfig的配置
HttpConfig是配置访问规则的,可以存在多个。
如果需要多个配置多个HttpConfig,可以使用多个静态内部类继承WebSecurityConfigurerAdapter来注册多个。
多个类需要使用@Order来指定执行顺序。数值越小,优先级越高。
1 |
|
1.6 加密密码
Spring Security提供了BCryptPasswordEncoder类,可以加密密码。
即使明文是一致的,生成的加密密码也会不一样,先来看一个简单的测试:
1 |
|
明文密码123生成的加密密码是不一样的。
1 | $2a$10$CuU3nXzZ.DtVmI5hxxNS1exrV0nSECqL182g/ockvp8tylmdYb67O |
我们可以在之前的配置类中使用加密密码:
1 |
|
1.7 方法级别的权限
可以使用@PreAuthorize、@PostAuthorize、@Secured三个注解对方法的访问进行限制。
同时需要配置@EnableGlobalMethodSecurity注解开启全局方法限制。
controller中需要访问service层的方法,@EnableGlobalMethodSecurity加在controller层。
1 |
|
service层中的方法中使用@PreAuthorize、@PostAuthorize、@Secured三个注解对方法的访问进行限制。
@PreAuthorize、@PostAuthorize可以使用表达式。
@Secured只能使用ROLE_角色名称,并且需要注意大小写。如果大小写写错,这个方法限制没生效,所有的用户都可以访问该方法。
1 |
|
1.8 角色继承
上级可能具备下级的所有权限,如果使用角色继承,这个功能就很好实现,我们只需要在 SecurityConfig 中添加如下代码来配置角色继承关系即可。
角色继承的配置只需要在配置类中,将RoleHierarchy加入到容器即可。
注意,在配置时,需要给角色手动加上
ROLE_前缀。下面的配置表示
ROLE_admin自动具备
ROLE_user的权限。
.
如果有多个角色继承关系的配置,使用空格分割开。比如ROLE_admin > ROLE_user ROLE_root > ROLE_user
但是SpringBoot 2.4.1的版本是使用\n分隔符,ROLE_admin > ROLE_user \n ROLE_root > ROLE_user
1 |
|
1.9 动态权限配置
前面我们配置的HttpConfig都是写在配置类中的,在真实的项目中,这显然不合理。
真实项目中,应该从把哪些角色可以访问哪些资源的信息保存到数据库中。修改了数据库的信息,就相当于动态修改了权限。
在之前的security库中添加menu和menu_role两张表。
menu代表着可以访问的资源的路径,menu_role代表着访问资源的路径和角色之间的关系。
1 | /*Table structure for table `menu` */ |
menu
id | pattern |
---|---|
1 | /dba/** |
2 | /admin/** |
3 | /user/** |
menu_role
id | mid | rid |
---|---|---|
1 | 1 | 1 |
2 | 2 | 2 |
3 | 3 | 3 |
新增实体类Menu:
一个资源可能有多个角色可以访问,有List
1 |
|
动态配置权限需要两个类:
根据当前的请求,从数据库中查询出哪些角色可以访问该资源
根据数据库中查询出来的角色和当前登录用户的角色进行对比,校验能否访问
RequestUrlFilter:
1 | /** |
Lambda写法
1 | /** |
MyAccessDecisionManager:
1 | /** |
在配置类中注入RequestUrlFilter,MyAccessDecisionManager
,并在config中配置它们,完整的配置类如下:
如果使用动态控制权限,就不能再配置anyRequest().antherticated()
1 |
|
2.0 从内存中获取登录信息
1 | Hr hr = (Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); |
2.1 JSON格式登陆
Spring Security默认是使用Key-Value的形式进行登陆的。也就是默认的/login请求只支持Key-Value的POST请求。
用户登陆的用户名/密码是在UsernamePasswordAuthenticationFilter
类中处理的。
1 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { |
从这段代码中,我们就可以看出来为什么 Spring Security 默认是通过 key/value 的形式来传递登录参数,因为它处理的方式就是 request.getParameter。
所以我们要定义成 JSON 的,思路很简单,就是自定义来定义一个过滤器代替 UsernamePasswordAuthenticationFilter
,重写attemptAuthentication方法.
JsonAuthenticationFilter:
定义一个过滤器继承UsernamePasswordAuthenticationFilter
.
1 | public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter { |
接下来要在配置类中创建自己的Filter类,并配置到HttpConfig中,filter中能设置的属性跟formLogin的差不多。Filter中设置的和formLogin设置的都能起效,但一般只需要设置一个即可,完整的配置类如下:
SecurityConfiguration
1 |
|
2.2 持久化令牌
当每次使用用户名/密码登陆的时候,会生成一个series和token,浏览器把它保存在cookie中。当退出浏览器再重新访问的时候,还是同一个cookie,并不会重新生成新的token。
而当在别端访问的时候,使用用户名/密码登陆的时候,会生成新的token,之前的浏览器cookie就会失效,从而实现多端踢下线的功能。
如果之前在别端访问过,又没有退出,是可以继续访问的。
如果使用Spring Security自带的JdbcPersistentTokenRepository,需要添加一张数据表,JdbcPersistentTokenRepository会操作这张表。
1 | CREATE TABLE `persistent_logins` ( |
配置类中:
1 |
|
2.2 自定义验证逻辑-验证码
首先,需要引入验证码的框架:
1 | <dependency> |
配置验证码,这里主要是设置一下验证码图片的宽高等。
1 |
|
接口返回验证码图片:
1 |
|
实现自己的AutherationProvider,继承自DaoAuthenticationProvider,因为所有的账户/密码登陆的方式都会经过这里,我们可以在这里添加验证码的逻辑。
1 | public class MyAuthenticationProvider extends DaoAuthenticationProvider { |
配置类中添加自己写的AuthenticationProvider,同时放开访问验证码图片的接口:
1 |
|
2.3 异常处理
当未登录直接访问url时,拦截器(DecisionManager)会抛出异常,并重定向到/login登录页面,此时前端是没有通过代理的,直接访问url,就会出现跨域的问题。
有一种解决方案是在/login上添加跨域的注解,但这不是很好,我希望后端返回未登录
的信息给前端,这就需要异常处理了。
1 |