springboot 整合springsecurity
序: 本文使用springboot+mybatis+SpringSecurity 實現資料庫動態的管理使用者、角色、許可權管理
本文細分角色和許可權,並將使用者、角色、許可權和資源均採用資料庫儲存,並且自定義濾器,代替原有的FilterSecurityInterceptor過濾器, 並分別實現AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService,並在配置檔案中進行相應配置。
spring security的簡單原理:
使用眾多的攔截器對url攔截,以此來管理許可權。但是這麼多攔截器,筆者不可能對其一一來講,主要講裡面核心流程的兩個。
首先,許可權管理離不開登陸驗證的,所以登陸驗證攔截器AuthenticationProcessingFilter要講; 還有就是對訪問的資源管理吧,所以資源管理攔截器AbstractSecurityInterceptor要講;
但攔截器裡面的實現需要一些元件來實現,所以就有了AuthenticationManager、accessDecisionManager等元件來支撐。
現在先大概過一遍整個流程,使用者登陸,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現,而且AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊(不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等),如果驗證通過後會將使用者的許可權資訊封裝一個User放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。 訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截,其中會呼叫FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部許可權,在呼叫授權管理器AccessDecisionManager,這個授權管理器會通過spring的全域性快取SecurityContextHolder獲取使用者的許可權資訊,還會獲取被攔截的url和被攔截url所需的全部許可權,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果許可權足夠,則返回,許可權不夠則報錯並呼叫許可權不足頁面。
重要
本文設計和程式碼是基於 上一篇部落格(請點選)
進行修改。
本文目錄: 1:資料庫表設計 2:許可權表的業務 3:springSecurity 配置修改 4:修改home.html 檔案 5:修改HomeController.java 檔案 6:測試檢驗
目錄結構如下:
1:資料庫表設計
由於本文增加了許可權表所以本文的資料庫表為5個分別是: 使用者表、角色表、許可權表、使用者角色中間表、角色許可權中間表
初始化資料
注意:Sys_permission 表的url萬用字元為兩顆星,比如說 /user下的所有url,應該寫成 /user/**; 許可權的名字可以隨意起名
- 1
- 2
insert into SYS_USER (id,username, password) values (1,'admin', 'admin');
insert into SYS_USER (id,username, password) values (2,'abel', 'abel');
insert into SYS_ROLE(id,name) values(1,'ROLE_ADMIN');
insert into SYS_ROLE(id,name) values(2,'ROLE_USER');
insert into SYS_ROLE_USER(SYS_USER_ID,ROLES_ID) values(1,1);
insert into SYS_ROLE_USER(SYS_USER_ID,ROLES_ID) values(2,2);
BEGIN;
INSERT INTO `Sys_permission` VALUES ('1', 'ROLE_HOME', 'home', '/', null), ('2', 'ROLE_ADMIN', 'ABel', '/admin', null);
COMMIT;
BEGIN;
INSERT INTO `Sys_permission_role` VALUES ('1', '1', '1'), ('2', '1', '2'), ('3', '2', '1');
COMMIT;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
2:許可權表的業務程式碼
2.1 java bean
Permission.java
package com.us.example.domain;
/**
* Created by yangyibo on 17/1/20.
*/
public class Permission {
private int id;
//許可權名稱
private String name;
//許可權描述
private String descritpion;
//授權連結
private String url;
//父節點id
private int pid;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescritpion() {
return descritpion;
}
public void setDescritpion(String descritpion) {
this.descritpion = descritpion;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
}
2.2 dao 層
在 com.us.example.dao 包下新建PermissionDao.java 檔案。
PermissionDao.java
package com.us.example.dao;
import com.us.example.config.MyBatisRepository;
import com.us.example.domain.Permission;
import java.util.List;
/**
* Created by yangyibo on 17/1/20.
*/
public interface PermissionDao {
public List<Permission> findAll();
public List<Permission> findByAdminUserId(int userId);
}
在src/resource/mapper目錄下新建對應的mapper.xml 檔案
PermissionDaoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.us.example.dao.PermissionDao">
<select id="findAll" resultType="com.us.example.domain.Permission">
SELECT * from Sys_permission ;
</select>
<select id="findByAdminUserId" parameterType="int" resultType="com.us.example.domain.Permission">
select p.*
from Sys_User u
LEFT JOIN sys_role_user sru on u.id= sru.Sys_User_id
LEFT JOIN Sys_Role r on sru.Sys_Role_id=r.id
LEFT JOIN Sys_permission_role spr on spr.role_id=r.id
LEFT JOIN Sys_permission p on p.id =spr.permission_id
where u.id=#{userId}
</select>
</mapper>
3:springSecurity 配置修改
3.1 修改 WebSecurityConfig.java
修改com.us.example.config包下的 WebSecurityConfig.java 檔案如下:
package com.us.example.config;
import com.us.example.service.CustomUserService;
import com.us.example.service.MyFilterSecurityInterceptor;
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.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.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* Created by yangyibo on 17/1/18.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Bean
UserDetailsService customUserService(){ //註冊UserDetailsService 的bean
return new CustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //user Details Service驗證
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何請求,登入後可以訪問
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.permitAll() //登入頁面使用者任意訪問
.and()
.logout().permitAll(); //登出行為任意訪問
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
}
3.2 修改CustomUserService
修改CustomUserService.java 內容如下:
package com.us.example.service;
import com.us.example.dao.PermissionDao;
import com.us.example.dao.UserDao;
import com.us.example.domain.Permission;
import com.us.example.domain.SysRole;
import com.us.example.domain.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 java.util.ArrayList;
import java.util.List;
/**
* Created by yangyibo on 17/1/18.
*/
@Service
public class CustomUserService implements UserDetailsService { //自定義UserDetailsService 介面
@Autowired
UserDao userDao;
@Autowired
PermissionDao permissionDao;
public UserDetails loadUserByUsername(String username) {
SysUser user = userDao.findByUserName(username);
if (user != null) {
List<Permission> permissions = permissionDao.findByAdminUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
for (Permission permission : permissions) {
if (permission != null && permission.getName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
//1:此處將許可權資訊新增到 GrantedAuthority 物件中,在後面進行全許可權驗證時會使用GrantedAuthority 物件。
grantedAuthorities.add(grantedAuthority);
}
}
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}
3.3 新增MyAccessDecisionManager
在com.us.example.service 包下新增 MyAccessDecisionManager.java 檔案
package com.us.example.service;
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.Service;
import java.util.Collection;
import java.util.Iterator;
/**
* Created by yangyibo on 17/1/19.
*/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否擁有許可權的決策方法,
//authentication 是釋CustomUserService中迴圈新增到 GrantedAuthority 物件中的許可權資訊集合.
//object 包含客戶端發起的請求的requset資訊,可轉換為 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 為MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法返回的結果,此方法是為了判定使用者請求的url 是否在許可權表中,如果在許可權表中,則返回給 decide 方法,用來判定使用者是否有此許可權。如果不在許可權表中則放行。
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 為在註釋1 中迴圈新增到 GrantedAuthority 物件中的許可權資訊集合
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
3.4 新增 MyFilterSecurityInterceptor
在com.us.example.service 包下新增 MyFilterSecurityInterceptor.java 檔案
package com.us.example.service;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* Created by yangyibo on 17/1/19.
*/
@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;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
3.5 新增 MyInvocationSecurityMetadataSourceService
在com.us.example.service 包下新增MyInvocationSecurityMetadataSourceService.java檔案
package com.us.example.service;
import com.us.example.dao.PermissionDao;
import com.us.example.domain.Permission;
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.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* Created by yangyibo on 17/1/19.
*/
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionDao permissionDao;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 載入許可權表中所有許可權
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = permissionDao.findAll();
for(Permission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
//此處只添加了使用者的名字,其實還可以新增更多許可權的資訊,例如請求方法到ConfigAttribute的集合中去。此處新增的資訊將會作為MyAccessDecisionManager類的decide的第三個引數。
array.add(cfg);
//用許可權的getUrl() 作為map的key,用ConfigAttribute的集合作為 value,
map.put(permission.getUrl(), array);
}
}
//此方法是為了判定使用者請求的url 是否在許可權表中,如果在許可權表中,則返回給 decide 方法,用來判定使用者是否有此許可權。如果不在許可權表中則放行。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含使用者請求的request 資訊
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
4:修改home.html 檔案
修改src/resources/templates目錄下 的home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta content="text/html;charset=UTF-8"/>
<title sec:authentication="name"></title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<style type="text/css">
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Spring Security演示</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a th:href="@{/}"> 首頁 </a></li>
<li><a th:href="@{/admin}"> admin </a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<h1 th:text="${msg.title}"></h1>
<p class="bg-primary" th:text="${msg.content}"></p>
<div sec:authorize="hasRole('ROLE_HOME')"> <!-- 使用者型別為ROLE_ADMIN 顯示 -->
<p class="bg-info" th:text="${msg.etraInfo}"></p>
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')"> <!-- 使用者型別為ROLE_ADMIN 顯示 -->
<p class="bg-info">恭喜您,您有 ROLE_ADMIN 許可權 </p>
</div>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="登出"/>
</form>
</div>
</div>
</body>
</html>
5:修改HomeController.java 檔案
package com.us.example.controller;
import com.us.example.domain.Msg;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Created by yangyibo on 17/1/18.
*/
@Controller
public class HomeController {
@RequestMapping("/")
public String index(Model model){
Msg msg = new Msg("測試標題","測試內容","歡迎來到HOME頁面,您擁有 ROLE_HOME 許可權");
model.addAttribute("msg", msg);
return "home";
}
@RequestMapping("/admin")
@ResponseBody
public String hello(){
return "hello admin";
}
}
6.測試檢驗
由於資料庫的配置 admin 使用者擁有 訪問 home和admin 頁面的許可權。
abel 使用者只有訪問 home 的許可權
使用admin 登入
點選 admin 按鈕 會反回結果 “hello admin“
使用abel 使用者登入 點選 點選 admin 按鈕 頁面會報403