文章目录
- 回顾
- 授权
- 创建项目
- 创建项目:security-mybatis-roles
- 数据库表设计
- mybatis 关联数据库配置
- 创建用户 User类和权限 Authority类
- 创建Mapper接口和映射xml
- 创建service 类UserDetailsServiceImpl 实现接口UserDetailsService
- 配置关联spring security
- 创建HelloController ,定义对应接口
- 启动测试
授权
用户如果要访问某一个资源,我们要去检查用户是否具备这样的权限,如果具备就允许访问,如果不具备,则不允许访问,这就是授权
我们通过案例讲解权限基于数据库的配置和权限的配置
创建项目
由于 Spring Security 支持多种数据源,例如内存、数据库、LDAP 等,这些不同来源的数据被共同封装成了一个 UserDetailService 接口,任何实现了该接口的对象都可以作为认证数据源
通过上一篇文章循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成 我们介绍了如何从数据库关联到spring security 用户认证的配置,今天,我们还是基于mybatis 读取数据库用户和权限来进行授权案例的讲解
创建项目:security-mybatis-roles
引入依赖:
具体依赖如下:
<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>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
增加了JSON的解析依赖,这样我们就可以基于前后端分离的方式,登录验证后返回JSON,如果不了解的同学,可以先学习下循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
数据库表设计
CREATE TABLE `h_user` (
`username` varchar(50) NOT NULL,
`password` varchar(500) NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
CREATE TABLE `h_authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `ix_auth_username` (`username`,`authority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
这里简单的准备了两张表,一张是用户表,只有三个字段用户名、密码、是否可用;还有一张权限表,只有两个字段,用户名和权限,用户和权限确定一条唯一的权限记录。一个用户可能存在多个权限,一个权限可能同时有多个用户,因此,用户和权限的关系是多对多的关系,
插入默认数据
INSERT INTO h_user
(username
, password
, enabled
) VALUES (‘harry’, ‘123456’, ‘1’);
INSERT INTO h_user
(username
, password
, enabled
) VALUES (‘mike’, ‘123456’, ‘1’);
INSERT INTO h_authorities
(username
, authority
) VALUES (‘harry’, ‘admin’);
INSERT INTO h_authorities
(username
, authority
) VALUES (‘harry’, ‘user’);
INSERT INTO h_authorities
(username
, authority
) VALUES (‘mike’, ‘user’);
这些数据中,用户:harry,具备有admin,user的权限,而mike ,只有user的权限
mybatis 关联数据库配置
创建用户 User类和权限 Authority类
public class User implements UserDetails {
private String password; //密码
private String username; //用户名
private boolean accountNonExpired=true;
private boolean accountNonLocked=true;
private boolean credentialsNonExpired=true;
private boolean enabled; //是否可用
private List<Authority>authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
public class Authority implements GrantedAuthority {
private String username;
//权限名称
private String authority;
@Override
public String getAuthority() {
return "ROLE_"+authority;
}
}
这里,一个用户可能有多个权限,因此,在用户的实体类中,有一个List权限的字段authorities,通过前面的学习,我们知道,用户要实现接口UserDetails ,当然也可以不用实现UserDetails ,但是最终也要转换,我们这里是图方便,就实现了接口UserDetails 。权限需要实现接口GrantedAuthority ,也是为了方便;
这里 getAuthority()返回的权限前加“ROLE_” 前缀,为什么要加这个前缀?主要是因为在进行配置权限时,默认spring security会自动在权限前加个前缀,所以我们从数据库读取权限出来时,因为我们存储的时候没有这前缀,因此要得加上
我们也可以跟踪配置到源码看看,确实也是会自动加上的前缀
创建Mapper接口和映射xml
@Mapper//指定这是一个操作数据库的mapper
public interface UserMapper {
//根据用户名查找用户
User findUserByUsername(String username);
}
<mapper namespace="com.harry.security.mapper.UserMapper">
<resultMap id="BaseUser" type="com.harry.security.entity.User" >
<id property="username" column="username" ></id>
<result property="password" column="password" ></result>
<result property="enabled" column="enabled" ></result>
<collection property="authorities" ofType="com.harry.security.entity.Authority">
<result property="username" column="username"/>
<result property="authority" column="authority"/>
</collection>
</resultMap>
<select id="findUserByUsername" resultMap="BaseUser" parameterType="string">
select u.username,u.`password`,u.enabled,hauth.authority from h_user u LEFT JOIN h_authorities hauth on hauth.username=u.username where u.username=#{username}
</select>
</mapper>
这里介绍一下,resultMap 中的collection 标签的配置,主要是为了将SQL联表查询权限映射为一个集合,这样一来,一个用户读取出来,就包含了他所有的权限集合
创建service 类UserDetailsServiceImpl 实现接口UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userByUsername = userMapper.findUserByUsername(username);
return userByUsername;
}
}
配置关联spring security
创建类SecurityConfig 继承 WebSecurityConfigurerAdapter ,并配置关联UserDetailsServiceImpl 从数据库读取,配置登录验证以JSON形式交互,同时配置两个接口的访问权限,凡是接口是/admin/** 类型的都要具备admin的权限才能访问,凡是/user/*类型要有user权限才能访问
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and().formLogin()
.successHandler((req,resp,authentication)->{
Object principal = authentication.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(principal));
out.flush();
out.close();
})
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(e));
out.flush();
out.close();
})
.permitAll()
.and().exceptionHandling()
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString("尚未登录,请先登录"));
out.flush();
out.close();
}
)
.and().logout().logoutSuccessHandler((req,resp,authentication)->{
Object principal = authentication.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(JSON.toJSONString(principal));
out.flush();
out.close();
})
.and().csrf().disable()
;
}
}
这里的匹配规则我们采用了 Ant 风格的路径匹配符,Ant 风格的路径匹配符在 Spring 家族中使用非常广泛,它的匹配规则也非常简单:
通配符 | 含义 |
** | 匹配多层路径 |
* | 匹配一层路径 |
? | 匹配任意单个字符 |
上面配置的含义是:
如果请求路径满足 /admin/** 格式,则用户需要具备 admin 角色。
如果请求路径满足 /user/** 格式,则用户需要具备 user 角色。
剩余的其他格式的请求路径,只需要认证(登录)后就可以访问。
另一方面,如果你强制将 anyRequest 配置在 antMatchers 前面,像下面这样:
http.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.and()
此时项目在启动的时候,就会报错,会提示不能在 anyRequest 之后添加 antMatchers:
这从语义上很好理解,anyRequest 已经包含了其他请求了,在它之后如果还配置其他请求也没有任何意义。
从语义上理解,anyRequest 应该放在最后,表示除了前面拦截规则之外,剩下的请求要如何处理。
在拦截规则的配置类 AbstractRequestMatcherRegistry 中,我们可以看到如下一些代码(部分源码):
public abstract class AbstractRequestMatcherRegistry<C> {
private boolean anyRequestConfigured = false;
public C anyRequest() {
Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
this.anyRequestConfigured = true;
return configurer;
}
public C antMatchers(HttpMethod method, String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
}
public C antMatchers(String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
}
protected final List<MvcRequestMatcher> createMvcMatchers(HttpMethod method,
String... mvcPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest");
return matchers;
}
public C regexMatchers(HttpMethod method, String... regexPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.regexMatchers(method, regexPatterns));
}
public C regexMatchers(String... regexPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns));
}
public C requestMatchers(RequestMatcher... requestMatchers) {
Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
return chainRequestMatchers(Arrays.asList(requestMatchers));
}
}
从这段源码中,可以看到,在任何antMatchers拦截规则之前(包括 anyRequest 自身),都会先判断 anyRequest 是否已经配置,如果已经配置,则会抛出异常,系统启动失败。
这样大家就理解了为什么 anyRequest 一定要放在最后。
创建HelloController ,定义对应接口
@RestController
public class HelloController {
// @PreAuthorize("admin")
@RequestMapping("/sayHello")
public String sayHello(){
return "十年生死两茫茫,不思量,自难忘----苏轼,hello "+getLoginUser();
}
@RequestMapping("/admin/home")
public String home(){
return "天生我材必有用,千金散尽还复来----李白,hello admin :"+getLoginUser();
}
@RequestMapping("/user/home")
public String userHome(){
return "滚滚长江东逝水----苏轼,hello user: "+getLoginUser();
}
//获取登录用户
private String getLoginUser(){
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}
这三个测试接口,我们的规划是这样的:
/sayHello 是任何人只要登录了都可以访问的接口
/admin/home 是具有 admin 身份的人才能访问的接口
/user/home 是具有 user 身份的人才能访问的接口
这样,因为我们数据库脚本中默认的harry 具有admin和user的权限,因此,harry是可以访问所有接口的,而mike 只有user的权限,因此,mike 只能访问/user/home 和/sayHello
接下来我们来见证奇迹
启动测试
启动项目,因为我们配置的是前后端分离以JSON串方式数据交互的,我这里演示是用postman,访问接口:http://127.0.0.1:8080/login 进行登录,先登录harry
可以看到,登录成功后,返回来的用户harry,具有两个权限admin 和user
我们来看看能访问哪些接口
经过测试发现,harry已经具备了访问这三个接口的权限了
接下里在看看mike,登录mike
mike登录成功,开始访问接口
可以发现,mike 因为没有admin的权限,因此无法访问到接口/admin/home,其他两个都能正常访问
OK,关于权限和数据库结合的配置,就介绍到这里