Spring Security 基於URL的許可權判斷

廢物大師兄發表於2021-12-06

1.  FilterSecurityInterceptor 原始碼閱讀

org.springframework.security.web.access.intercept.FilterSecurityInterceptor

通過過濾器實現對HTTP資源進行安全處理。

該安全攔截器所需的 SecurityMetadataSource 型別為 FilterInvocationSecurityMetadataSource。 

doFilter方法中直接呼叫invoke方法

基本都是呼叫父類的方法,那下面就重點看下父類 AbstractSecurityInterceptor 中相關方法

為安全物件實現安全攔截的抽象類。

AbstractSecurityInterceptor 將確保安全攔截器的正確啟動配置。 它還將實現對安全物件呼叫的正確處理,即:

  1. 從 SecurityContextHolder 獲取 Authentication 物件。
  2. 通過在SecurityMetadataSource中查詢安全物件請求,確定請求是與安全呼叫還是公共呼叫相關(PS:簡單地來講,就是看一下請求的資源是不是受保護的,受保護的就是安全呼叫,就要許可權,不受保護的就不需要許可權就可以訪問)。
  3. 對於受保護的呼叫(有一個用於安全物件呼叫的 ConfigAttributes 列表):
    1. 如果 Authentication.isAuthenticated() 返回 false,或者 alwaysReauthenticate 為 true,則根據配置的 AuthenticationManager 對請求進行身份驗證。 通過身份驗證後,將 SecurityContextHolder 上的 Authentication 物件替換為返回值。
    2.  根據配置的AccessDecisionManager授權請求。
    3. 通過配置的RunAsManager執行任何run-as替換。
    4. 將控制權傳遞迴具體的子類,它實際上將繼續執行物件。返回一個 InterceptorStatusToken 以便在子類完成物件的執行後,其 finally 子句可以確保 AbstractSecurityInterceptor 被呼叫並使用 finallyInvocation(InterceptorStatusToken) 正確處理。
    5. 具體的子類將通過 afterInvocation(InterceptorStatusToken, Object) 方法重新呼叫 AbstractSecurityInterceptor。
    6. 如果 RunAsManager 替換了 Authentication 物件,則將 SecurityContextHolder 返回到呼叫 AuthenticationManager 後存在的物件。
    7. 如果定義了AfterInvocationManager,則呼叫它並允許它替換將要返回給呼叫者的物件。
  4.  對於公開的呼叫(安全物件呼叫沒有 ConfigAttributes):
    1. 如上所述,具體的子類將返回一個 InterceptorStatusToken,在執行完安全物件後,該 InterceptorStatusToken 隨後被重新呈現給 AbstractSecurityInterceptor。 AbstractSecurityInterceptor 在它的 afterInvocation(InterceptorStatusToken, Object) 被呼叫時不會採取進一步的行動。
  5. 控制再次返回到具體的子類,以及應該返回給呼叫者的物件。然後子類會將該結果或異常返回給原始呼叫者。

下面具體來看

從這裡我們可以知道返回null和空集合是一樣的。

接下來看授權

這是我們要重點關注的,可以看到,授權靠的是 accessDecisionManager.decide(authenticated, object, attributes) 

因此,我們想要實現自己的基於請求Url的授權只需自定義一個 AccessDecisionManager 即可

接下來,我們來具體實現一下

2.  自定義基於url的授權

先把Spring Security授權的大致流程流程擺在這兒:

 

自定義FilterInvocationSecurityMetadataSource

package com.example.security.core;

import com.example.security.service.SysPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/2
 */
@Component
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private SysPermissionService sysPermissionService;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();
        String httpMethod = fi.getRequest().getMethod();

        List<ConfigAttribute> attributes = new ArrayList<>();

        Map<String, String> urlRoleMap = sysPermissionService.getAllUrlRole();

        for (Map.Entry<String, String> entry : urlRoleMap.entrySet()) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                return SecurityConfig.createList(entry.getValue());
            }
        }

        // 返回null和空列表是一樣的,都表示當前訪問的資源不需要許可權,所有人都可以訪問
        return attributes;
//        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

這裡需要說明一下,其實Spring Security裡面說的role不一定表示的是我們自己建的那個角色表,我們可以這樣理解,就是它這裡的所謂role只是一個許可權標識。我們在建表的時候,通常最基本的是5張表(使用者表、角色表、許可權表、使用者角色關係表、角色許可權關係表),我們可以把受保護的資源(通常是一個url)與角色關聯起來,建立哪些角色可以訪問哪些資源,也可以直接判斷資源的許可權(通常是許可權編碼/標識)。

只要有這個關係,剩下的就是寫法不同而已。如果你把role理解成資源的許可權標識的話,那麼返回的Collection<ConfigAttribute>中就最多有一個元素,如果理解成角色的話,那麼可能有多個元素。就這麼點兒東西,寫法不同而已,本質是一樣的。

自定義AccessDecisionManager

package com.example.security.core;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/2
 */
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        System.out.println(authorities);
        System.out.println(configAttributes);
        //  檢視當前使用者是否有對應的許可權訪問該保護資源
        for (ConfigAttribute attribute : configAttributes) {
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(attribute.getAttribute())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("Access is denied");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

decide方法的三個引數,依次表示:

  • 呼叫者(非空)
  • 被呼叫的安全物件
  • 與被呼叫的安全物件關聯的配置屬性

配置WebSecurityConfig

package com.example.security.config;

import com.example.security.core.MyAccessDecisionManager;
import com.example.security.core.MyFilterSecurityMetadataSource;
import com.example.security.core.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    private MyFilterSecurityMetadataSource myFilterSecurityMetadataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .and()
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(myFilterSecurityMetadataSource);
                        object.setAccessDecisionManager(myAccessDecisionManager);
                        return object;
                    }
                })
                .anyRequest().authenticated();
    }



    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

其它不重要的就直接貼出來了

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo126?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
  jpa:
    database: mysql
    show-sql: true

SysPermissionEntity.java

package com.example.security.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Getter
@Setter
@Entity
@Table(name = "sys_permission")
public class SysPermissionEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    /** 許可權編碼(標識) */
    private String code;

    /** 許可權名稱 */
    private String name;

    /** 許可權URL */
    private String url;

}

SysRoleEntity.java

package com.example.security.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Getter
@Setter
@Entity
@Table(name = "sys_role")
public class SysRoleEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    /** 角色編碼 */
    private String code;

    /** 角色名稱 */
    private String name;

    @ManyToMany
    @JoinTable(name = "sys_role_permission", joinColumns = {@JoinColumn(name = "role_id")}, inverseJoinColumns = {@JoinColumn(name = "permission_id")})
    private Set<SysPermissionEntity> permissions;
}

SysUserEntity.java

package com.example.security.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Getter
@Setter
@Entity
@Table(name = "sys_user")
public class SysUserEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    /** 使用者名稱 */
    private String username;

    /** 密碼 */
    private String password;

    @ManyToMany
    @JoinTable(name = "sys_user_role",
            joinColumns = {@JoinColumn(name = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "role_id")})
    private Set<SysRoleEntity> roles;
}

SysUserRepository.java

package com.example.security.repository;

import com.example.security.entity.SysUserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
public interface SysUserRepository extends JpaRepository<SysUserEntity, Integer>, JpaSpecificationExecutor<SysUserEntity> {

    SysUserEntity findByUsername(String username);
}

SysPermissionServiceImpl.java 

package com.example.security.service.impl;

import com.example.security.entity.SysPermissionEntity;
import com.example.security.repository.SysPermissionRepository;
import com.example.security.service.SysPermissionService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Service
public class SysPermissionServiceImpl implements SysPermissionService {

    @Resource
    private SysPermissionRepository sysPermissionRepository;

    @Override
    public Map<String, String> getAllUrlRole() {
        List<SysPermissionEntity> list = sysPermissionRepository.findAll();
        return list.stream().collect(Collectors.toMap(SysPermissionEntity::getUrl, SysPermissionEntity::getCode));
    }
}

MyUserDetails.java

package com.example.security.domain;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetails implements UserDetails {

    private String username;
    private String password;
    private boolean enabled;
    private Set<SimpleGrantedAuthority> 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 true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

MyUserDetailsService.java

package com.example.security.core;

import com.example.security.domain.MyUserDetails;
import com.example.security.entity.SysPermissionEntity;
import com.example.security.entity.SysUserEntity;
import com.example.security.repository.SysUserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.transaction.Transactional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@Transactional
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Resource
    private SysUserRepository sysUserRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
        if (null == sysUserEntity) {
            throw new UsernameNotFoundException("使用者不存在");
        }
        Set<SimpleGrantedAuthority> authorities = sysUserEntity.getRoles().stream()
                .flatMap(roleId->roleId.getPermissions().stream())
                .map(SysPermissionEntity::getCode)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());

        return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), true, authorities);
    }
}

HelloController.java

package com.example.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author ChengJianSheng
 * @Date 2021/12/6
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/sayHello")
    public String sayHello() {
        return "Hello";
    }

    @GetMapping("/sayHi")
    public String sayHi() {
        return "Hi";
    }
}

資料庫指令碼如下

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '許可權編碼(標識)',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '許可權名稱',
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '許可權URL',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 'home', '首頁', '/home/**');
INSERT INTO `sys_permission` VALUES (2, 'user:add', '新增使用者', '/user/add');
INSERT INTO `sys_permission` VALUES (3, 'user:delete', '刪除使用者', '/user/delete');
INSERT INTO `sys_permission` VALUES (4, 'hello:sayHello', '打招呼', '/hello/sayHello');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色編碼',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名稱',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'employee', '員工');
INSERT INTO `sys_role` VALUES (2, 'engineer', '工程師');
INSERT INTO `sys_role` VALUES (3, 'leader', '組長');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  `permission_id` int(11) NOT NULL COMMENT '許可權ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 1, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2, 1);
INSERT INTO `sys_role_permission` VALUES (3, 2, 2);
INSERT INTO `sys_role_permission` VALUES (4, 3, 1);
INSERT INTO `sys_role_permission` VALUES (5, 3, 2);
INSERT INTO `sys_role_permission` VALUES (6, 3, 3);
INSERT INTO `sys_role_permission` VALUES (7, 3, 4);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '使用者名稱',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'zhangsan', '$2a$10$e4wFsFHQCNjPe5tTJMPkRuKGwmMGC45pfjMupY9nwbTuoKQ0bKc/u');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '使用者ID',
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1, 1);
INSERT INTO `sys_user_role` VALUES (2, 1, 2);
INSERT INTO `sys_user_role` VALUES (3, 1, 3);

SET FOREIGN_KEY_CHECKS = 1;

瀏覽器訪問 http://localhost:8080/hello/sayHi 正常返回,不用登入,因為沒有在sys_permission表中配置該資源,也就是說它不是一個受保護的資源(公開資源)

訪問http://localhost:8080/hello/sayHello則需要先登入,用zhangsan登入成功以後正確返回 

專案結構如下

 

相關文章