基于RBAC的权限管理
RBAC(Role-Based Access Control):基于角色的访问控制
当前项目中,RBAC具体的表现为:
- 管理员表:ams_admin
- 角色表:ams_role
- 权限表:ams_permission
- 管理员与角色的关联表:ams_admin_role
- 角色与权限的关联表:ams_role_permission
Spring Security框架
关于Spring Security框架
Spring Security框架主要解决了认证与授权相关的问题。
认证信息:表示用户的身份的信息
认证:识别用户身份信息,具体可以表现为“登录”
授权:授予用户权限,使之可以进行某些访问,反之,如果用户没有得到相关授权,就不允许进行某些访问
Spring Security框架的依赖项
在Spring Boot项目中使用Spring Security时需要添加依赖项:
<!-- Spring Boot支持Spring Security的依赖项,用于处理认证与授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Security的典型特征
当添加了spring-boot-starter-security依赖项,会自带一系列的自动配置,当启动项目后,相比此前的项目,会有以下变化:
-
所有的请求(包括根本不存在的)都是必须要登录的,如果未登录,会自动跳转到框架自带的登录页面
-
默认的用户名是
user,密码是启用项目时在控制台提示的一串UUID值- 登录时,如果在打开登录页面后重启过服务器端,应该刷新登录页面,否则,第1次输入并提交是无效的
-
当登录成功后,会自动跳转到此前尝试访问的URL
-
当登录成功后,可通过
/logout退出登录 -
默认不接受普通的
POST请求,如果提交POST请求,会响应403(Forbidden)- 具体原因参见后续的
CSRF相关内容
- 具体原因参见后续的
关于Spring Security的配置
在项目的根包下创建SecurityConfiguration类,作为Spring Security的配置类,继承自WebSecurityConfigurerAdpater类,并重写void configure(HttpSecurity http)方法,在方法体中进行配置:
@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); // 不要保留调用父类同名方法的代码,不要保留!不要保留!不要保留!
}
}
关于默认的登录表单
在配置类的void configure(HttpSecurity http)方法中,在没有调用父级的同名方法时,默认是不启用登录表单的!
如果需要启用登录表单,需要在方法中自行调用http.formLogin(),例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 如果调用以下方法,当需要访问通过认证的资源,但是未通过认证时,将自动跳转到登录页面
// 如果未调用以下方法,将响应403
http.formLogin();
// super.configure(http); // 不要保留调用父类同名方法的代码,不要保留!不要保留!不要保留!
}
关于请求的访问控制
在配置类的void configure(HttpSecurity http)方法中,调用参数对象的authroizeRequests()方法可开启对请求进行授权,例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 白名单
// 使用1个星号,表示通配此层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/delete
// 但是,不可以匹配多个层级,例如:/admins/*,不可以匹配:/admins/9527/delete
// 使用2个连续的星号,表示通配若干层级的任意资源,例如:/admins/*,可以匹配:/admins/add-new、/admins/9527/delete
String[] urls = {
"/doc.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources",
"/v2/api-docs",
};
// 基于请求的访问控制
http.authorizeRequests() // 对请求进行授权
.mvcMatchers(urls) // 匹配某些路径
.permitAll() // 直接许可,即不需要认证即可访问
.anyRequest() // 任意请求
.authenticated(); // 要求通过认证的
}
**注意:**以上对请求授权的配置是遵循“第一匹配原则”的!例如,假设存在以下配置:
http.authorizeRequests()
.mvcMatchers("/test")
.authenticated()
.mvcMatchers("/test")
.permitAll();
按照以上配置,/test是“需要通过认证才可以访问的”!
**注意:**在配置请求授权时,调用anyRequest()表示“任意请求”,即“所有请求”,由于以上代码将anyRequest()配置在偏后的位置,也可以理解为“除了以上配置过的请求以外的所有请求”!
**注意:**在开发实践中,应该将更加具体的URL或请求配置在靠前的位置,将使用了通配符的,或使用anyRequest()匹配的请求配置在靠后的位置。
使用临时的自定义的账号实现登录
在使用Spring Security框架时,可以自定义组件类,实现UserDetailsService接口,则Spring Security框架会基于此类的对象来处理认证!
在项目的根包下创建security.UserDetailsServiceImpl类,在类上添加@Service,实现UserDetailsService接口,重写接口中的方法:
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return null;
}
}
当项目中存在UserDetailsService类型的组件对象时,尝试登录时,Spring Security框架会自动使用登录表单中的用户名来调用以上loadUserByUsername()方法,并且,得到此方法返回的UserDetails类型的结果,此结果中应该包含用户的相关信息,例如用户名、密码、账号状态等,接下来,Spring Security框架会自动使用登录表单中的密码与返回的UserDetails中的密码进行对比,并判断账号的状态,以此决定表单提交的登录信息是否可以通过认证。
所以,以上loadUserByUsername()方法的实现中,只需要完成“根据用户名返回匹配的UserDetails对象”即可!例如:
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 假设允许登录的账号是:root / 123456
if (!"root".equals(s)) {
return null;
}
UserDetails userDetails = User.builder()
.username("root")
.password("123456")
.disabled(false) // 账号禁用
.accountLocked(false) // 账号锁定
.accountExpired(false) // 账号过期
.credentialsExpired(false) // 凭证过期
.authorities("这是一个临时使用的山寨权限") // 权限
.build();
return userDetails;
}
}
当项目中存在UserDetailsService类型的组件对象时,启用项目时控制台中将不再显示user账号的UUID密码,并且,user账号也不再可用!
**注意:**Spring Security框架在处理登录信息时,默认要求所有密码都是通过某种密码编码器处理过后的,如果使用的密码是明文的,必须明确的指出!例如,在配置类中通过@Bean方法配置NoOpPasswordEncoder,例如:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
使用数据库中的账号信息实现登录
首先,需要实现“根据用户名查询用户信息”的查询功能,需要执行的SQL语句大致是:
SELECT id, username, password, enable FROM ams_admin WHERE username=?
在pojo.vo包下创建AdminLoginInfoVO类:
package cn.tedu.csmall.passport.pojo.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 管理员的登录信息的VO类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Data
public class AdminLoginInfoVO implements Serializable {
/**
* 数据id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码(密文)
*/
private String password;
/**
* 是否启用,1=启用,0=未启用
*/
private Integer enable;
}
在AdminMapper.java接口中添加抽象方法:
AdminLoginInfoVO getLoginInfoByUsername(String username);
在AdminMapper.xml中配置以上抽象方法映射的SQL语句:
<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
SELECT
<include refid="LoginInfoQueryFields"/>
FROM
ams_admin
WHERE
username=#{username}
</select>
<sql id="LoginInfoQueryFields">
<if test="true">
id, username, password, enable
</if>
</sql>
<resultMap id="LoginInfoResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="enable" property="enable"/>
</resultMap>
在AdminMapperTests中编写并执行测试:
@Test
void getLoginInfoByUsername() {
String username = "root";
Object queryResult = mapper.getLoginInfoByUsername(username);
log.debug("根据用户名【{}】查询数据详情完成,查询结果:{}", username, queryResult);
}
然后,调整UserDetailsServiceImpl中的实现:
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("xxx");
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
if (loginInfo == null) {
return null;
}
UserDetails userDetails = User.builder()
.username(loginInfo.getUsername())
.password(loginInfo.getPassword()) // 期望是密文
.disabled(loginInfo.getEnable() == 0) // 账号禁用
.accountLocked(false) // 账号锁定
.accountExpired(false) // 账号过期
.credentialsExpired(false) // 凭证过期
.authorities("这是一个临时使用的山寨权限") // 权限
.build();
return userDetails;
}
由于数据库中的测试数据的密码都是密文的,例如:
$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C
以上密文是通过BCrypt算法进行编码的结果!为了保证Spring Security能够正确的判断密码,需要将密码编码器改为BCrypt的密码编码器,例如:
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
关于伪造的跨域攻击
**伪造的跨域攻击:**此类攻击原理是利用服务器端对客户端浏览器的“信任”来实现的!当在某个客户端的浏览器的第1个选项卡登录后,如果在第2个或其它选项卡中访问同一个服务器端,均会被视为“已登录”的状态!假设,你在第1个选项卡登录了你的网上银行,然后,在第2个选项卡中打开了某个坏人的网站,这个坏人的网站中隐藏了一个向银行发起转账的链接(例如把请求的链接设置为<img>标签的src属性值),网上银行收到第2个选项卡中发出的链接的请求,仍会认为这是一个正常的请求,会尝试执行转账操作!这种攻击方式就称之为“伪造的跨域攻击”。当然,实际情况是不可能实现网上银行转账的,但是,仍可能利用这样的机制实施某些攻击行为,例如窃取数据。
**典型的防御手段:**在“非前后端分离”的开发模式下,当服务器端生成表单时,会在表单中隐藏一个具有“唯一性”较强的“随机值”,例如UUID值,当客户端正常提交表单时,此值会随着表单一起提交到服务器端,服务器端会对比收到的此值是否为此前生成的值,以Spring Security的登录表单为例:
在“前后端分离”的项目中,由于服务器端不负责生成各表单页面,也就无法在表单中添加UUID值,则提交请求时,就不可能提交正确的UUID值,所以,这种防御机制在前后端分离的项目中并不适用!
在Spring Security的配置类中,在void configurer(HttpSecurity http)方法中,调用参数对象的csrf().disable()即可禁用默认的防御机制,例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用“防止伪造的跨域攻击”的防御机制
http.csrf().disable();
// 暂不关心其它代码
}