1. 程式人生 > >SpringBoot整合SpringSecurity(附帶原始碼)

SpringBoot整合SpringSecurity(附帶原始碼)

SpringBoot整合SpringSecurity

配置環境

配置idea

我使用的是idea,點選New Project
在這裡插入圖片描述點選next專案資訊配置隨意,再下一步選上下圖所示的元件
在這裡插入圖片描述

配置thymeleaf

在pom.xml中加入

<dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
 </dependency>

在application.yml中加入

spring:
	thymeleaf:
    	suffix: .html
    	cache: false
    	//設定為傳統模式,防止因為嚴格的語法檢測遇到的各種麻煩,例如<html />後習慣不會去加斜槓就會被當做錯誤檢測
    	mode: LEGACYHTML5

配置傳統檢測模式需要額外匯入上述的dependency並配合配置檔案

配置JPA

在application.yml中加入

spring:

  datasource:
    url: jdbc:mysql://localhost:3306/springsecurity
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

  jpa:
    show-sql: true
    //由於jpa預設將駝峰命名的entity轉化為帶下劃線的名稱去匹配資料庫中的表名,而我在資料庫中也是使用駝峰命名,所以需要下入下列的配置
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    

配置資料庫

匯入如下程式碼


DROP TABLE IF EXISTS `Sys_permission`;

CREATE TABLE `Sys_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  `description` varchar(200) DEFAULT NULL,
  `url` varchar(200) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_permission` WRITE;
/*!40000 ALTER TABLE `Sys_permission` DISABLE KEYS */;

INSERT INTO `Sys_permission` (`id`, `name`, `description`, `url`, `pid`)
VALUES
	(1,'ROLE_HOME','index','/',NULL),
	(2,'ROLE_ADMIN','admin','/admin',NULL),
	(3,'ROLE_USER','user','/user',NULL);

/*!40000 ALTER TABLE `Sys_permission` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_permission_role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_permission_role`;

CREATE TABLE `Sys_permission_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) unsigned NOT NULL,
  `permission_id` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `role2` (`role_id`),
  KEY `permission` (`permission_id`),
  CONSTRAINT `permission` FOREIGN KEY (`permission_id`) REFERENCES `Sys_permission` (`id`),
  CONSTRAINT `role2` FOREIGN KEY (`role_id`) REFERENCES `Sys_Role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_permission_role` WRITE;
/*!40000 ALTER TABLE `Sys_permission_role` DISABLE KEYS */;

INSERT INTO `Sys_permission_role` (`id`, `role_id`, `permission_id`)
VALUES
	(10,2,1),
	(11,2,3),
	(12,3,1),
	(13,3,2),
	(15,2,2);

/*!40000 ALTER TABLE `Sys_permission_role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_Role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_Role`;

CREATE TABLE `Sys_Role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_Role` WRITE;
/*!40000 ALTER TABLE `Sys_Role` DISABLE KEYS */;

INSERT INTO `Sys_Role` (`id`, `name`)
VALUES
	(2,'ROLE_USER'),
	(3,'ROLE_ADMIN');

/*!40000 ALTER TABLE `Sys_Role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_Role_User
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_Role_User`;

CREATE TABLE `Sys_Role_User` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `Sys_User_id` int(11) unsigned NOT NULL,
  `Sys_Role_id` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user` (`Sys_User_id`),
  KEY `role` (`Sys_Role_id`),
  CONSTRAINT `role` FOREIGN KEY (`Sys_Role_id`) REFERENCES `Sys_Role` (`id`),
  CONSTRAINT `user` FOREIGN KEY (`Sys_User_id`) REFERENCES `Sys_User` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_Role_User` WRITE;
/*!40000 ALTER TABLE `Sys_Role_User` DISABLE KEYS */;

INSERT INTO `Sys_Role_User` (`id`, `Sys_User_id`, `Sys_Role_id`)
VALUES
	(6,1,3),
	(7,2,2);

/*!40000 ALTER TABLE `Sys_Role_User` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_User
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_User`;

CREATE TABLE `Sys_User` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(200) DEFAULT NULL,
  `password` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_User` WRITE;
/*!40000 ALTER TABLE `Sys_User` DISABLE KEYS */;

INSERT INTO `Sys_User` (`id`, `username`, `password`)
VALUES
	(1,'admin','6d789d4353c72e4f625d21c6b7ac2982'),
	(2,'user','36f1cab655c5252fc4f163a1409500b8');

/*!40000 ALTER TABLE `Sys_User` ENABLE KEYS */;
UNLOCK TABLES;

最後會生成5個表,分別是使用者表,角色表,許可權表,使用者角色中間表,角色許可權中間表。

配置包目錄

在這裡插入圖片描述
其中entity可以用idea進行生成,
在這裡插入圖片描述
在這裡插入圖片描述

配置dao層

需要新建UserDao和PermissionDao兩個類

/**
* UserDao
*/
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
    //自定義一個根據姓名查詢使用者的方法
    public User findByUsername(String userName);
}


/**
* PermissionDao
* 無需自定義方法,直接使用jpa封裝好的就可以
*/
public interface PermissionDao extends JpaRepository<Permission,Integer> {
}

配置Entity關係

在本demo中需要配置兩個關係,分別是使用者與角色的多對多關係,角色和許可權的多對多關係。
在User類中加入:

private List<Role> roles = new ArrayList<>();
/**
     * @ManyToMany 表示多對多關係,fetch = FetchType.EAGER配置懶載入策略為立即載入,因為多對多涉及到樹形結構的第二層,
     * 使用懶載入會在使用roles物件時才去資料庫查詢,但是在本專案中會出現no session,暫時無法解決,所以加上次配置
     *
     * @JoinTable  name:中間表名, @joinColumn : name:在中間表中對應外來鍵名,referencedColumnName在原先表中的主鍵名
     *
     * inverseJoinColumns中的@joinColumn : name:多的另一方在中間表中對應的主鍵名,referencedColumnName在原先表中的主鍵名
     *
     * 此處的配置表明user和role的多對多關係由user維護
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "Sys_Role_User", joinColumns = {@JoinColumn(name = "Sys_User_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "Sys_Role_id", referencedColumnName = "id")})
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

在Role中加入:

private List<User> users = new ArrayList<>();
//mappedBy:對映的名字為user中role集合的名字
 @ManyToMany(mappedBy = "roles")
    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }

Permission和Role的多對多對映也是如此,就不貼出來了。

簡單的環境搭建就到此結束了,一切從簡所以沒有配置連線池。

SpringSecurity配置

我們先做個簡單的嘗試,在配置好以上步驟後,在template中加入一個名為index的頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>登入成功</h1>

<h2><a href="/logout">退出</a></h2>
</body>
</html>

並建立一個LoginController,在controller中設定對映路徑為/index。
我們啟動專案,在位址列中輸入localhost:8080/index,你會發現自動跳轉到了一個登陸介面,我們完全沒有寫過 登陸介面,所以這個是springsecurity自帶的一個登入頁,登陸的使用者名稱為user,密碼是輸出在console中的uuid字串。
在這裡插入圖片描述登陸以後就可以訪問Index頁面了。在我們配置Security之前,它預設攔截所有頁面並會自動生成一個登陸的賬號密碼,但這顯然不是我們想要的樣子。下面我們對它進行改造。

配置攔截策略

首先我們在security包下建立一個類,名字為WebSecurityConfig,繼承WebSecurityConfigurerAdapter

//這兩個註解缺一不可
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //重寫引數為HttpSecurity的configure方法,配置攔截策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        		//自定義登陸頁面
                .formLogin().loginPage("/login")
                	//登陸成功後跳轉的頁面
                .defaultSuccessUrl("/index")
                	//登陸失敗或無許可權跳轉頁面
                .failureUrl("/login-error")
                .permitAll()
                	//其他所有頁面必須驗證後才可以訪問
                .and().authorizeRequests().anyRequest().authenticated()
                //不加上不驗證。不知道為什麼
                .and().csrf().disable();

    }

}

引用網上的圖片
在這裡插入圖片描述順帶建立兩個HTML

login.html

<form  class="form-signin" action="/login" method="post">
    <h2 class="form-signin-heading">使用者登入</h2>
    <table>
        <tr>
            <td>使用者名稱:</td>
            <td><input type="text" name="username"  class="form-control"  placeholder="請輸入使用者名稱"/></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"  class="form-control" placeholder="請輸入密碼" /></td>
        </tr>
        <tr>

            <td colspan="2">
                <button type="submit"  class="btn btn-lg btn-primary btn-block" >登入</button>
            </td>
        </tr>
    </table>
</form>
error.html

<h1>error</h1>

此時再執行專案,輸入localhost:8080/跳轉到的頁面就是我們剛才寫好的頁面了,隨便輸入賬號密碼點選登入的報錯頁面也是剛剛新增的error頁面。

自定義登陸賬號驗證

實際開發中我們需要在資料庫中儲存使用者的賬號密碼資訊,所以我們需要自定義驗證方式。
在security資料夾中建立MyUserDetialsService類 實現UserDetailsService介面

@Service
public class MyUserDetialsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //此處的user是entity包中的user
        com.security.entity.User user = userDao.findByUsername(userName);
        if (user != null) {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            //獲取使用者的角色集合
            List<Role> roles = user.getRoles();
            //遍歷角色集合,並獲取每個角色擁有的許可權
            for (Role role : roles) {
                List<Permission> permissions = role.getPermissions();
                for (Permission permission :permissions) {
                    //為每個授權中心物件寫入許可權名
                    grantedAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
                }
            }
            /**此處的user是springsecurity中的一個實現了UserDetails介面的user類,因為我們沒有將entity中的user去實現
             * UserDetails介面,所以只能在此處呼叫實現好的構造方法
             */
            return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
        }
        return null;
    }
}

此處我們的密碼使用MD5配合加密鹽進行加密,所以需要在utils包中建立MD5Utils類

public class MD5Util {

    private static final String SALT = "tamboo";

    public static String encode(String password) {
        password = password + SALT;
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        char[] charArray = password.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }
}

在先前建立好的WebSecurityConfig中加入如下配置:

//注入我們剛才寫好的service類
@Autowired
    MyUserDetialsService userService;

    //配置加密
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            //加密
            @Override
            public String encode(CharSequence rawPassword) {
                return MD5Util.encode((String) rawPassword);
            }

            //解密,前者是輸入的密碼,後者是資料庫查詢的密碼
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(MD5Util.encode((String) rawPassword));
            }
        });
    }

實際上我們剛才寫的程式碼中並沒有對密碼進行驗證,SpringSecurity中已經在內部寫好了驗證程式碼,我們只需要將查詢到的user物件轉換為UserDetail物件返回給框架即可。此時再次執行demo登陸的賬號密碼就可以使用資料庫中自定義的了,我目前設定的預設賬號密碼為admin:admin,user:user。

配置自定義許可權驗證

在security包下建立MyInvocationSecurityMetadataSourceService實現FilterInvocationSecurityMetadataSource介面。該類用於載入許可權表中的url資訊,並和request的url進行對比,有匹配則將該URL所需要的許可權返回給decide()方法,不存在則返回空

@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private PermissionDao permissionDao;

    private HashMap<String, Collection<ConfigAttribute>> map = null;

    //在demo啟動第一個使用者登陸後,載入所有許可權進map
    public void loadResourceDefine() {
        map = new HashMap<>();
        Collection<ConfigAttribute> array;
        ConfigAttribute cfg;
        List<Permission> permissions = permissionDao.findAll();
        for (Permission permission : permissions) {
            array = new ArrayList<>();
            //此處只添加了使用者的名字,其實還可以新增更多許可權的資訊,例如請求方法到ConfigAttribute的集合中去。此處新增的資訊將會作為MyAccessDecisionManager類的decide的第三個引數。
            cfg = new SecurityConfig(permission.getName());
            array.add(cfg);
            //用許可權的getUrl() 作為map的key,用ConfigAttribute的集合作為 value
            map.put(permission.getUrl(), array);
        }
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if(map ==null) {
            loadResourceDefine();
        }
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        //遍歷許可權表中的url
        for (String url : map.keySet()) {
            matcher = new AntPathRequestMatcher(url);
            //與request對比,符合則說明許可權表中有該請求的URL
            if(matcher.matches(request)) {
                return map.get(url);
            }
        }
        return null;
    }

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

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

接著在security下新建MyAccessDecisionManager類實現AccessDecisionManager介面。該類為決策類,決策該使用者的request是否有許可權訪問。

@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

    /**
     * @param authentication UserService中迴圈新增到GrantedAuthority中的許可權資訊集合
     * @param object         包含客戶端發起的請求的request資訊,可以轉換為HTTPRequest
     * @param collection     url所需的許可權集合
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        //判斷URL所需的許可權集合是否為空,為空則放行
        if (null == collection || collection.size() <= 0) {
            return;
        }
        String needPermission;
        for (ConfigAttribute c : collection) {
            //獲得所需的許可權
            needPermission = c.getAttribute();
            //遍歷使用者擁有的許可權與URL所需的許可權進行對比
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needPermission.trim().equals(ga.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("no permission");
    }

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

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

最後在security包下建立MyFilterSecurityInterceptor類

@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {


    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    //設定決策器
    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }


    public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi裡面有一個被攔截的url
//裡面呼叫MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有許可權
//再呼叫MyAccessDecisionManager的decide方法來校驗使用者的許可權是否足夠
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//執行下一個攔截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }


    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;

    }

    //新增判斷url所需的許可權類
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

建立

admin.html
<p class="bg-info">Admin許可權訪問</p>
index2.html
<p>這是沒錄入資料庫的url</p>
user.html
<p>User許可權訪問</p>

將這些頁面加入controller對映。
資料庫中設定的關係為:

使用者 角色 許可權
admin ROLE_USER,ROLE_ADMIN ROLE_ADMIN,ROLE_HOME,ROLE_User
user ROLE_USER ROLE_HOME,ROLE_User

在這裡插入圖片描述許可權表中對映的URL如圖。
執行demo
結果如下:
登陸admin賬號,所有頁面都可以訪問
登陸user賬號,除了/admin無許可權訪問,其他都可以訪問。
/index2沒有錄入資料庫,但是在任何使用者登陸以後都可以訪問。

原始碼地址:

https://gitee.com/king176/springbootdemo_source_code/tree/master