3.1 授权简介
用户认证,比如用户名密码、验证码等都是为了让系统知道是谁在访问系统。而能在系统做什么事叫做授权,或者叫鉴权、权限控制。
SpringSecurity对授权的定义:安全权限控制问题其实就是控制能否访问url。
3.1.1 授权原理
在我们应用系统里面,如果想要控制用户权限,需要有2部分数据:
- 系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问。
- 另一份数据就是用户权限信息:请求用户拥有权限 。系统用户发送一个请求:系统配置信息和用户权限信息作比对,如果比对成功则允许访问。
当一个系统授权规则比较简单,基本不变时候,系统的权限配置信息可以写在我们的代码里面去的。比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成,如果权限复杂, 例如办公OA, 电商后台 管理系统等就不能使用写在代码里面了. 需要RBAC权限模型设计。
3.2 Spring Security授权
3.2.1 内置权限表达式
Spring Security 使用Spring EL来支持,主要用于Web访问和方法安全上, 可以通过表达式来判断是
否具有访问权限. 下面是Spring Security常用的内置表达式. ExpressionUrlAuthorizationConfifigurer定
义了所有的表达式
表达式 | 说明 |
---|---|
permitAll | 指定任何人都允许访问。 |
denyAll | 指定任何人都不允许访问 |
anonymous | 指定匿名用户允许访问。 |
rememberMe | 指定已记住的用户允许访问。 |
authenticated | 指定任何经过身份验证的用户都允许访问,不包含 anonymous |
fullyAuthenticated | 指定由经过身份验证的用户允许访问,不包含 anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用户允许访问, 会自动在角色 前面插入’ROLE_’ |
hasAnyRole([role1,role2]) | 指定需要任意一个角色的用户允许访问, 会自动在角 色前面插入’ROLE_’ |
hasAuthority(authority) | 指定需要特定的权限的用户允许访问 |
hasAnyAuthority([authority,authority]) | 指定需要任意一个权限的用户允许访问 |
hasIpAddress(ip) | 指定需要特定的IP地址可以访问 |
3.2.2 url 安全表达式
- 在SpringSecurity中,可以在配置类中配置url的访问权限,什么角色可以访问什么url。配置如下: ```java @Autowired MyAccessDeniedHandler myAccessDeniedHandler;
@Override protected void configure(HttpSecurity http) throws Exception { // 设置/user开头的请求需要ADMIN角色权限 http.authorizeRequests().antMatchers(“/user/“).hasRole(“ADMIN”); // 设置/product开头的请求只有PRODUCT、ADMIN之一权限且访问ip为127.0.0.1才能访问 http.authorizeRequests().antMatchers(“/product/“) .access(“hasAnyRole(‘PRODUCT’,’ADMIN’) and hasIpAddress(‘127.0.0.1’)”);
// 自定义权限异常的返回
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
}
2. 在以上所示代码中,access()是定义多个访问权限要求,hasRole("ADMIN")表示拥有ADMIN权限的用户才能访问/user/**这个url,而权限的定义则在我们自定义的用户密码验证类中来完成。
```java
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
@Autowired
UserService userService;
/**
* 根据用户名查询用户
* @param username 前端传的账号名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
return null;
}
// 权限集合 框架底层是匹配配置文件中定义的ROLE
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (Objects.equals(username,"admin")) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
} else {
authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
}
// noop表示不进行密码加密(明文认证)
return new org.springframework.security.core.userdetails.User(
username,"{bcrypt}" + user.getPassword(),
// 用户是否启用
true,
// 用户是否过期
true,
// 用户凭证是否过期
true,
// 用户是否锁定
true,
authorities);
}
}
- 因为hasRole()的方法会自动在ADMIN前面加上ROLE_,所以在添加权限时也需要加上。还可以自定义一个权限不足的返回信息,避免系统的403页面对用户不友好。定义完成在配置类中引入,如步骤1。
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("权限不足!请联系管理员");
}
}
3.2.3 在Web 安全表达式中引用自定义Bean授权
自定义web中url的访问权限返回值,返回true表示有权访问,返回false表示无权访问
@Component
public class MyAuthorizationServiceImpl {
/**
* 检查用户是否授权
* @param authentication 认证信息
* @param request 请求对象
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();
// 如果用户名等于admin,直接返回true
if (Objects.equals(username,"admin")) {
return true;
} else {
String requestUrl = request.getRequestURI();
// 循环判断用户的权限集合是否包含ROLE_ADMIN
if (requestUrl.contains("/user")) {
for (GrantedAuthority authority : authorities) {
if ("ROLE_ADMIN".equals(authority.getAuthority())) {
return true;
}
}
}
// 循环判断用户的权限集合是否包含ROLE_PRODUCT
if (requestUrl.contains("/product")) {
for (GrantedAuthority authority : authorities) {
if ("ROLE_PRODUCT".equals(authority.getAuthority())) {
return true;
}
}
}
}
return false;
}
}
//使用自定义Bean授权
http.authorizeRequests().antMatchers("/user/**")
.access("@MyAuthorizationServiceImpl.check(authentication,request)");
public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {
return id <= 10;
}
// 使用自定义Bean授权,并携带路径参数
http.authorizeRequests().antMatchers("/user/delete/{id}")
.access("@MyAuthorizationServiceImpl.check(authentication,request,#id)");
3.2.4 Method安全表达式
针对方法级别的访问控制比较复杂, spring security 提供了4种注解分别是 @PreAuthorize、 @PostAuthorize 、@PreFilter、 @PostFilter。.
开启方法级别的注解配置,在security配置类中添加注解
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
在方法上使用注解
@PreAuthorize : 注解适合进入方法前的权限验证
@RequestMapping("/findAll")
@PreAuthorize("hasRole('ADMIN')")
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
return "user_list";
}
@PostAuthorize: @PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限, Spring EL 提供返回对象能够在表达式语言中获取到返回对象的 returnObject、 ```java /**
- 根据ID查询用户
- @return */ @GetMapping(“/{id}”) @ResponseBody // returnObject : 代表return返回的值 @PostAuthorize(“returnObject.username== authentication.principal.username”) // 判断查询用户信息是否是当前登录用户信息.否则没有 权限 public User getById(@PathVariable Integer id) { User user = userService.getById(id); return user; } ```
- @PreFilter: 可以用来对集合类型的参数进行过滤, 将不符合条件的元素剔除集合 ```java /**
- 商品删除-多选删除 *
- @return
*/
@GetMapping(“/delByIds”)
@PreFilter(filterTarget = “ids”, value = “filterObject%2==0”)
//剔除参数为 基数的值
public String delByIds(@RequestParam(value = “id”) List
ids) { for (Integer id : ids) {
} return “redirect:/user/findAll”; } ```System.out.println(id);
- @PostFilter: 可以用来对集合类型的返回值进行过滤, 将不符合条件的元素剔除集合 ```java /**
- 查询所有用户-返回json数据 *
- @return
*/
@RequestMapping(“/findAllTOJson”)
@ResponseBody
@PostFilter(“filterObject.id%2==0”)
// 剔除返回值ID为偶数的值
public List
findAllTOJson() { List userList = userService.list(); return userList; } ```
3.3 基于数据库的RBAC数据模型的权限控制
我们开发一个系统,必然面临权限控制的问题,不同的用户具有不同的访问、操作、数据权限。 形成理论的权限控制模型有:自主访问控制(DAC: Discretionary Access Control)、强制访问控制 (MAC: Mandatory Access Control)、基于属性的权限验证(ABAC: Attribute-Based Access Control)等。最常被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control)
3.3.1 RBAC权限模型简介
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:
- 用户:系统接口及访问的操作者
- 权限:能够访问某接口或者做某操作的授权资格
- 角色:具有一类相同操作权限的总称
RBAC权限模型核心授权逻辑如下:
- 用户与权限直接关联
用户与权限直接关联可以很简单表达出用户的权限,但是新增的用户每次都要重新赋予权限,即使是权限与前面的用户一致,就显得很冗余,所以就出现了角色,每个角色的权限分开,用户只需要绑定角色就可以有该角色的权限。
- 用户与角色关联
这样只需要维护角色和权限之间的关系就可以了. 如果业务员的权限发生变更, 只需要变动业务员
角色和权限之前的关系进行维护就可以了. 用户和权限就分离开来了. 如下图
3.3.3 基于RBAC设计权限表结构
- 一个用户有一个或多个角色
- 一个角色包含多个用户
- 一个角色有多种权限
- 一个权限属于多个角色
3.3.3 基于SpringSecurity实现RBAC权限管理
动态查询数据库用户对应的权限
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 根据用户ID查询权限
*
* @param id
* @return
*/
@Select("SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,t_user u " +
"WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id =#{id}")
List<Permission> findByUserId(Integer id);
}
给登录用户授权
// 权限集合 框架底层是匹配配置文件中定义的ROLE
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 基于数据库查询用户对应的权限
List<Permission> permissions = permissionService.findByUserId(user.getId());
for (Permission permission : permissions) {
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
设置访问权限
// 查询数据库所有权限列表
List<Permission> permissionList = permissionService.list();
// 添加请求权限
for (Permission permission : permissionList) {
http.authorizeRequests().antMatchers(permission.getPermissionUrl())
.hasAuthority(permission.getPermissionTag());
}
3.4 基于页面端标签的权限控制
在jsp页面或者thymeleaf模板页面中我们可以使用spring security提供的权限标签来进行权限控制.要
想使用thymeleaf为SpringSecurity提供的标签属性,首先需要引入thymeleaf-extras-springsecurity依
赖支持。
在pom 文件中的引入springsecurity的标签依赖thymeleaf-extras-springsecurity5。
<!--添加thymeleaf为SpringSecurity提供的标签 依赖 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version>
</dependency>
在html中引用
!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
3.4.1 常用SpringSecurity的标签属性介绍
- 判断用户是否已经登陆认证,引号内的参数必须是isAuthenticated()。
- sec:authorize=”isAuthenticated()”
- 获得当前用户的用户名,引号内的参数必须是name。 sec:authentication=“name”
- 判断当前用户是否拥有指定的权限。引号内的参数为权限的名称。 sec:authorize=“hasRole(‘role’)”
3.4.2 SpringSecurity标签的使用
<div class="leftnav">
<div class="leftnav-title">
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name"></span>
<img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/> </div>
</div>
<div sec:authorize="hasAuthority('user:findAll')">
<h2><span class="icon-user"></span>系统管理</h2>
<ul style="display:block">
<li>
<a href="/user/findAll" target="right">
<span class="icon-caret- right"></span>
用户管理
</a>
</li>
<li>
<a href="javascript:void(0)" onclick="toCors()" target="right">
<span class="icon-caret-right"></span>
跨域测试
</a></li>
</ul>
</div>
<div sec:authorize="hasAuthority('product:findAll')">
<h2><span class="icon-pencil-square-o"></span>数据管理</h2>
<ul>
<li>
<a href="/product/findAll" target="right">
<span class="icon- caret-right"></span>
商品管理
</a>
</li>
</ul>
</div>
</div>