spring-security-demo授权.zip

3.1 授权简介

用户认证,比如用户名密码、验证码等都是为了让系统知道是谁在访问系统。而能在系统做什么事叫做授权,或者叫鉴权、权限控制。
SpringSecurity对授权的定义:安全权限控制问题其实就是控制能否访问url。

3.1.1 授权原理

授权原理.jpg
在我们应用系统里面,如果想要控制用户权限,需要有2部分数据:

  1. 系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问。
  2. 另一份数据就是用户权限信息:请求用户拥有权限 。系统用户发送一个请求:系统配置信息和用户权限信息作比对,如果比对成功则允许访问。

当一个系统授权规则比较简单,基本不变时候,系统的权限配置信息可以写在我们的代码里面去的。比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成,如果权限复杂, 例如办公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 安全表达式

  1. 在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’)”);

  1. // 自定义权限异常的返回
  2. http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);

}

  1. 2. 在以上所示代码中,access()是定义多个访问权限要求,hasRole("ADMIN")表示拥有ADMIN权限的用户才能访问/user/**这个url,而权限的定义则在我们自定义的用户密码验证类中来完成。
  2. ```java
  3. @Service
  4. public class MyUserDetailServiceImpl implements UserDetailsService {
  5. @Autowired
  6. UserService userService;
  7. /**
  8. * 根据用户名查询用户
  9. * @param username 前端传的账号名
  10. * @return
  11. * @throws UsernameNotFoundException
  12. */
  13. @Override
  14. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  15. User user = userService.findByUsername(username);
  16. if (user == null) {
  17. return null;
  18. }
  19. // 权限集合 框架底层是匹配配置文件中定义的ROLE
  20. Collection<GrantedAuthority> authorities = new ArrayList<>();
  21. if (Objects.equals(username,"admin")) {
  22. authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
  23. } else {
  24. authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
  25. }
  26. // noop表示不进行密码加密(明文认证)
  27. return new org.springframework.security.core.userdetails.User(
  28. username,"{bcrypt}" + user.getPassword(),
  29. // 用户是否启用
  30. true,
  31. // 用户是否过期
  32. true,
  33. // 用户凭证是否过期
  34. true,
  35. // 用户是否锁定
  36. true,
  37. authorities);
  38. }
  39. }
  1. 因为hasRole()的方法会自动在ADMIN前面加上ROLE_,所以在添加权限时也需要加上。还可以自定义一个权限不足的返回信息,避免系统的403页面对用户不友好。定义完成在配置类中引入,如步骤1。
    1. @Component
    2. public class MyAccessDeniedHandler implements AccessDeniedHandler {
    3. @Override
    4. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    5. response.setContentType("text/html;charset=UTF-8");
    6. response.getWriter().write("权限不足!请联系管理员");
    7. }
    8. }

3.2.3 在Web 安全表达式中引用自定义Bean授权

自定义web中url的访问权限返回值,返回true表示有权访问,返回false表示无权访问

  1. @Component
  2. public class MyAuthorizationServiceImpl {
  3. /**
  4. * 检查用户是否授权
  5. * @param authentication 认证信息
  6. * @param request 请求对象
  7. * @return
  8. */
  9. public boolean check(Authentication authentication, HttpServletRequest request) {
  10. UserDetails userDetails = (UserDetails) authentication.getPrincipal();
  11. String username = userDetails.getUsername();
  12. Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();
  13. // 如果用户名等于admin,直接返回true
  14. if (Objects.equals(username,"admin")) {
  15. return true;
  16. } else {
  17. String requestUrl = request.getRequestURI();
  18. // 循环判断用户的权限集合是否包含ROLE_ADMIN
  19. if (requestUrl.contains("/user")) {
  20. for (GrantedAuthority authority : authorities) {
  21. if ("ROLE_ADMIN".equals(authority.getAuthority())) {
  22. return true;
  23. }
  24. }
  25. }
  26. // 循环判断用户的权限集合是否包含ROLE_PRODUCT
  27. if (requestUrl.contains("/product")) {
  28. for (GrantedAuthority authority : authorities) {
  29. if ("ROLE_PRODUCT".equals(authority.getAuthority())) {
  30. return true;
  31. }
  32. }
  33. }
  34. }
  35. return false;
  36. }
  37. }
  1. //使用自定义Bean授权
  2. http.authorizeRequests().antMatchers("/user/**")
  3. .access("@MyAuthorizationServiceImpl.check(authentication,request)");
  1. public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {
  2. return id <= 10;
  3. }
  1. // 使用自定义Bean授权,并携带路径参数
  2. http.authorizeRequests().antMatchers("/user/delete/{id}")
  3. .access("@MyAuthorizationServiceImpl.check(authentication,request,#id)");

3.2.4 Method安全表达式

针对方法级别的访问控制比较复杂, spring security 提供了4种注解分别是 @PreAuthorize、 @PostAuthorize 、@PreFilter、 @PostFilter。.

  1. 开启方法级别的注解配置,在security配置类中添加注解

    1. @Configuration
    2. @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解支持
    3. public class SecurityConfig extends WebSecurityConfigurerAdapter {}
  2. 在方法上使用注解

  • @PreAuthorize : 注解适合进入方法前的权限验证

    1. @RequestMapping("/findAll")
    2. @PreAuthorize("hasRole('ADMIN')")
    3. public String findAll(Model model) {
    4. List<User> userList = userService.list();
    5. model.addAttribute("userList", userList);
    6. return "user_list";
    7. }
  • @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) {
    1. System.out.println(id);
    } return “redirect:/user/findAll”; } ```
  • @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.2 RBAC的演化进程

  1. 用户与权限直接关联

用户与权限.jpg
用户与权限直接关联可以很简单表达出用户的权限,但是新增的用户每次都要重新赋予权限,即使是权限与前面的用户一致,就显得很冗余,所以就出现了角色,每个角色的权限分开,用户只需要绑定角色就可以有该角色的权限。

  1. 用户与角色关联

用户与角色.jpg
这样只需要维护角色和权限之间的关系就可以了. 如果业务员的权限发生变更, 只需要变动业务员
角色和权限之前的关系进行维护就可以了. 用户和权限就分离开来了. 如下图
1651823596(1).jpg

3.3.3 基于RBAC设计权限表结构

  • 一个用户有一个或多个角色
  • 一个角色包含多个用户
  • 一个角色有多种权限
  • 一个权限属于多个角色

1652080789(1).jpg

3.3.3 基于SpringSecurity实现RBAC权限管理

  1. 动态查询数据库用户对应的权限

    1. public interface PermissionMapper extends BaseMapper<Permission> {
    2. /**
    3. * 根据用户ID查询权限
    4. *
    5. * @param id
    6. * @return
    7. */
    8. @Select("SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,t_user u " +
    9. "WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id =#{id}")
    10. List<Permission> findByUserId(Integer id);
    11. }
  2. 给登录用户授权

    1. // 权限集合 框架底层是匹配配置文件中定义的ROLE
    2. Collection<GrantedAuthority> authorities = new ArrayList<>();
    3. // 基于数据库查询用户对应的权限
    4. List<Permission> permissions = permissionService.findByUserId(user.getId());
    5. for (Permission permission : permissions) {
    6. authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
    7. }
  3. 设置访问权限

    1. // 查询数据库所有权限列表
    2. List<Permission> permissionList = permissionService.list();
    3. // 添加请求权限
    4. for (Permission permission : permissionList) {
    5. http.authorizeRequests().antMatchers(permission.getPermissionUrl())
    6. .hasAuthority(permission.getPermissionTag());
    7. }

3.4 基于页面端标签的权限控制

在jsp页面或者thymeleaf模板页面中我们可以使用spring security提供的权限标签来进行权限控制.要
想使用thymeleaf为SpringSecurity提供的标签属性,首先需要引入thymeleaf-extras-springsecurity依
赖支持。

  1. 在pom 文件中的引入springsecurity的标签依赖thymeleaf-extras-springsecurity5。

    1. <!--添加thymeleaf为SpringSecurity提供的标签 依赖 -->
    2. <dependency>
    3. <groupId>org.thymeleaf.extras</groupId>
    4. <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version>
    5. </dependency>
  2. 在html中引用

    1. !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标签的使用

    1. <div class="leftnav">
    2. <div class="leftnav-title">
    3. <div sec:authorize="isAuthenticated()">
    4. <span sec:authentication="name"></span>
    5. <img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/> </div>
    6. </div>
    7. <div sec:authorize="hasAuthority('user:findAll')">
    8. <h2><span class="icon-user"></span>系统管理</h2>
    9. <ul style="display:block">
    10. <li>
    11. <a href="/user/findAll" target="right">
    12. <span class="icon-caret- right"></span>
    13. 用户管理
    14. </a>
    15. </li>
    16. <li>
    17. <a href="javascript:void(0)" onclick="toCors()" target="right">
    18. <span class="icon-caret-right"></span>
    19. 跨域测试
    20. </a></li>
    21. </ul>
    22. </div>
    23. <div sec:authorize="hasAuthority('product:findAll')">
    24. <h2><span class="icon-pencil-square-o"></span>数据管理</h2>
    25. <ul>
    26. <li>
    27. <a href="/product/findAll" target="right">
    28. <span class="icon- caret-right"></span>
    29. 商品管理
    30. </a>
    31. </li>
    32. </ul>
    33. </div>
    34. </div>