簡介
SpringBoot集成Security框架,實現用戶角色功能權限管理,很簡單的,確實這要得益于SpringBoot簡化配置的優點。
這節我們要實現通過數據庫表管理用戶角色權限,dao層使用MyBatis,如果對MyBatis比較陌生,建議先去了解一下,或者去看我的這篇文章《SpringBoot集成MyBatis》
準備數據表
我們還是采用經典的角色權限5張表:
1、用戶表(sys_user):id、username、password
2、角色表(sys_role):id、name
3、權限表(sys_permission):id、name、descript、url、pid
4、用戶角色表(sys_user_role):id、uid、rid
5、角色權限表(sys_role_permission):id、rid、pid
插入數據:
sys_user表
sys_role表
sys_permission表
sys_user_role表
sys_role_permission表
集成Security
-
1.引入maven依賴
我們會用到視圖層解析,這里用Thymeleaf去解析。
<!--Security權限管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
-
2.配置.properties文件
你也可以將 security.basic.enabled=true 打開,會有基礎登錄彈窗彈出。這里我們暫時不需要,所以將它關閉,之后我們自己實現登錄界面。
#Security
security.basic.enabled=false
security.user.name=admin
security.user.password=123456
#thymeleaf start
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.check-template-location=true
spring.thymeleaf.template-resolver-order=1
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.mode=HTML5
- 3.創建權限管理相關類
以下創建的幾個類包括:
CustomUserServiceImpl類
實現了UserDetailsService接口,這個接口很簡單就一個方法
UserDetails loadUserByUsername(String var1)
會返回一個用戶基本信息和該用戶擁有的權限功能 (by username),作為后續權限認證的依據,也是和Dao層通信的主要接口。
MyInvocationSecurityMetadataSourceService類
實現了FilterInvocationSecurityMetadataSource接口,類似Holder主要實現加載緩存權限功能路徑和名稱,以及提供從請求路徑查找權限名稱,供后續權限決策管理器去判定使用。
MyAccessDecisionManager類
實現了AccessDecisionManager接口,主要判定用戶是否擁有權限的決策方法,有權限放行,無權限拒絕訪問。
MyFilterSecurityInterceptor類
繼承AbstractSecurityInterceptor抽象類,權限管理Security真正的攔截器,并綁定了MyAccessDecisionManager(處理權限認證)和MyInvocationSecurityMetadataSourceService(提供請求路徑和權限名稱元數據)
WebSecurityConfig類
繼承WebSecurityConfigurerAdapter抽象類,主要是最后配置Security,配置之前定義的攔截器、提供用戶基本權限信息、以及一些訪問控制。
代碼示例:
CustomUserServiceImpl
import com.yu.scloud.baseframe.frame.dao.SysPermissionMapper;
import com.yu.scloud.baseframe.frame.dao.SysUserMapper;
import com.yu.scloud.baseframe.frame.model.SysPermission;
import com.yu.scloud.baseframe.frame.model.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;
@Service
public class CustomUserServiceImpl implements UserDetailsService { //自定義UserDetailsService 接口
@Autowired
SysUserMapper userDao;
@Autowired
SysPermissionMapper permissionDao;
//返回 user和user擁有的權限功能 by username
public UserDetails loadUserByUsername(String username) {
SysUser user = userDao.findByUserName(username);
if (user != null) {
List<SysPermission> permissions = permissionDao.findByAdminUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
for (SysPermission 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!");
}
}
}
MyInvocationSecurityMetadataSourceService
import com.yu.scloud.baseframe.frame.dao.SysPermissionMapper;
import com.yu.scloud.baseframe.frame.model.SysPermission;
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.*;
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private SysPermissionMapper permissionDao;
//緩存
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加載權限表中所有權限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<SysPermission> permissions = permissionDao.findAll();
for(SysPermission 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;
}
}
MyAccessDecisionManager
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;
@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;
}
}
MyFilterSecurityInterceptor
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;
@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;
}
}
WebSecurityConfig
import com.yu.scloud.baseframe.frame.service.impl.CustomUserServiceImpl;
import com.yu.scloud.baseframe.frame.service.impl.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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Bean
UserDetailsService customUserService(){ //注冊UserDetailsService 的bean
return new CustomUserServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //user Details Service驗證
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(new String[]{"/plugins/**"}).permitAll()//
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.permitAll() //登錄頁面用戶任意訪問
.and()
.logout().permitAll()
.and()
.headers()
.frameOptions().sameOrigin(); ; //注銷行為任意訪問
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
//解決靜態資源被攔截的問題(plugins目錄在工程resources/static/下)
web.ignoring().antMatchers("/plugins/**","/login.html");
}
}
-
4.創建MyBatis方式,相關實體類、Dao、Service以及Controller
實體類以及部分Dao我就不貼出來了,查看最前面的數據庫表字段就可以。或者使用MyBatis自動代碼生成器生成,快捷方便,如果還沒掌握它怎么使用,請看我的一篇文章《MyBatis代碼自動生成器》
SysUserMapper.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.yu.scloud.baseframe.frame.dao.SysUserMapper">
<select id="findByUserName" parameterType="String" resultType="com.yu.scloud.baseframe.frame.model.SysUser">
SELECT * FROM sys_user WHERE username=#{username};
</select>
</mapper>
SysPermissionMapper.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.yu.scloud.baseframe.frame.dao.SysPermissionMapper">
<select id="findAll" resultType="com.yu.scloud.baseframe.frame.model.SysPermission">
SELECT * from sys_permission;
</select>
<select id="findByAdminUserId" parameterType="int" resultType="com.yu.scloud.baseframe.frame.model.SysPermission">
select p.*
from sys_user u
LEFT JOIN sys_user_role sur on u.id= sur.uid
LEFT JOIN sys_role r on sur.rid=r.id
LEFT JOIN sys_role_permission srp on srp.rid=r.id
LEFT JOIN sys_permission p on p.id =srp.pid
where u.id=#{userId}
</select>
</mapper>
HomeController.java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
@RequestMapping("/")
public String index(Model model){
model.addAttribute("msg", "Hi~歡迎");
return "home";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/admin")
@ResponseBody
public String hello(){
return "hello admin";
}
}
- 5.創建視圖資源
創建login.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>登錄頁面</title>
<link rel="stylesheet" type="text/css" href="plugins/bootstrap/dist/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>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<!--/*@thymesVar id="logout" type=""*/-->
<p th:if="${param.logout}" class="bg-warning">已成功注銷</p><!-- 1 -->
<p th:if="${param.error}" class="bg-danger">有錯誤,請重試</p> <!-- 2 -->
<h2>使用賬號密碼登錄</h2>
<form name="form" th:action="@{/login}" action="/login" method="POST"> <!-- 3 -->
<div class="form-group">
<label>賬號</label>
<input type="text" class="form-control" name="username" value="" placeholder="賬號" />
</div>
<div class="form-group">
<label>密碼</label>
<input type="password" class="form-control" name="password" placeholder="密碼" />
</div>
<input type="submit" id="login" value="Login" class="btn btn-primary" />
</form>
</div>
</div>
</body>
</html>
創建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" type="text/css" th:href="@{plugins/bootstrap/dist/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}"></h1>
<div sec:authorize="hasRole('ROLE_HOME')"> <!-- 用戶類型為ROLE_ADMIN 顯示 -->
<p class="bg-info" th:text="${msg}">你好</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>
OK!Security集成完畢,啟動服務,訪問一下吧。
輸入用戶名:admin 密碼:admin 成功登錄到home界面:
點擊admin按鈕則順利跳轉到admin頁面。
假如注銷后登錄其它用戶 :用戶名:yu 密碼:123,則點擊admin按鈕提示權限不足無法進入admin頁面,說明集成Security權限管理成功。