Spring boot security 动态角色权限

原来项目一直使用spring mvc用的一套动态权限角色控制,现在项目更换spring boot来开发,原来的权限控制不起作用,于是使用spring boot security来实现动态角色和权限的配置功能。现功能只是为了实现cms后台的控制,还没有去做接口的动态权限控制,如果下一步需要并实现了功能会分享出来。
spring boot 使用的是2.1.0.RELEASE版本,废话不多说上配置文件。
pom.xml

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.simmytech.cms</groupId>
<artifactId>cms-platform</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>cms-platform</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath />
</parent>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>cms</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>

<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>

新建项目后看一下项目目录结构


目录结构

权限控制我们需要写的类有几个
1:com.simmytech.cms.config下的MyPasswordEncoder
2:com.simmytech.cms.config下的WebSecurityConfig
3:com.simmytech.cms.service下的CustomUserService
4:com.simmytech.cms.service下的MyAccessDecisionManager
5:com.simmytech.cms.service下的MyFilterSecurityInterceptor
6:com.simmytech.cms.service下的MyInvocationSecruityMetadataSourceService
下面我们先数据库用户、角色、权限表的设计和数据库的逻辑操作,然后再和secrity结合控制

数据库表一共由
数据库表

5个表组成,分别是用户表sys_user
用户表

角色表sys_role
角色表

权限表sys_permission
权限表

权限角色关联表sys_permission_role
权限角色关联表

角色用户关联表sys_role_user
角色用户关联表

现在看具体权限相关类的写法
MyPasswordEncoder类用户用户密码加密的类

package com.simmytech.cms.config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class MyPasswordEncoder implements PasswordEncoder {
Log log = LogFactory.getLog(MyPasswordEncoder.class);
BCryptPasswordEncoder bean = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence arg0) {
// TODO Auto-generated method stub
return bean.encode(arg0).toString();
}
@Override
public boolean matches(CharSequence arg0, String arg1) {
// TODO Auto-generated method stub
return bean.matches(arg0, arg1);
}
}

WebSecurityConfig类Security是否过滤的配置
package com.simmytech.cms.config;
import org.springframework.beans.factory.annotation.Autowired;
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.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;
import com.simmytech.cms.service.MyFilterSecurityInterceptor;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Autowired
UserDetailsService customUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService).passwordEncoder(new MyPasswordEncoder()); // user
// Details
// Service验证
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable()
.and().authorizeRequests().antMatchers("/dist/", "/plugins/","/favicon.ico").permitAll().anyRequest().authenticated() // 任何请求,登录后可以访问
.and().formLogin().loginPage("/login").defaultSuccessUrl("/").failureUrl("/login?error").permitAll() // 登录页面用户任意访问
.and().logout().permitAll(); // 注销行为任意访问
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
}

我的配置是可以iframe访问,静态资源的访问地址不拦截,登录地址,登录成功地址和登录失败的地址配置,其他访问地址全部需要经过权限拦截。
CustomUserService类登录根据用户名获取用户信息

package com.simmytech.cms.service;
import java.util.ArrayList;
import java.util.List;
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 com.simmytech.cms.model.SysPermission;
import com.simmytech.cms.model.SysUser;
@Service
public class CustomUserService implements UserDetailsService { // 自定义UserDetailsService
@Autowired
private UserService userService;
public UserDetails loadUserByUsername(String username) {
SysUser user = userService.getUserByUserName(username);
if (user != null) {
List<SysPermission> permissions = userService.getPermissionByUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (SysPermission permission : permissions) {
if (permission != null && permission.getName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
grantedAuthorities.add(grantedAuthority);
}
}
userService.updateUserLogintime(user.getId());
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}

MyAccessDecisionManager类

package com.simmytech.cms.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;
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
throw new AccessDeniedException("当前访问没有权限");
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

MyFilterSecurityInterceptor类

package com.simmytech.cms.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;
@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;
}
}

MyInvocationSecurityMetadataSourceService类

package com.simmytech.cms.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.Service;
import com.simmytech.cms.model.SysPermission;
import com.simmytech.cms.model.SysRole;
@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Value("${simmytech.exurl}")
private String exurl;
@Autowired
private UserService userService;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取当前访问url
String url = ((FilterInvocation) object).getRequestUrl();
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
if(filter(url)){
return null;
}
List<ConfigAttribute> result = new ArrayList<>();
// 查询数据库url匹配的菜单
List<SysPermission> menuList = userService.getPermissionByUrl(url);
if (menuList != null && menuList.size() > 0) {
for (SysPermission menu : menuList) {
// 查询拥有该菜单权限的角色列表
List<SysRole> roles = userService.getRoleByPermissionId(menu.getId());
if (roles != null && roles.size() > 0) {
for (SysRole role : roles) {
ConfigAttribute conf = new SecurityConfig(role.getName());
result.add(conf);
}
}
}
}
if(result.isEmpty()){
result.add(new SecurityConfig("ROLE_EMPTY"));
}
return result;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
private boolean filter(String url){
String[] urls=exurl.split(",");
for(String temp:urls){
if(url.indexOf(temp)==0){
return true;
}
}
return false;
}
}

下面开始看controller和登录页的配置

package com.simmytech.cms.web;
import java.util.Calendar;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.simmytech.cms.model.SysUser;
import com.simmytech.cms.service.UserService;
import com.simmytech.cms.vo.MenuVo;
@Controller
public class HomeController {
@Autowired
private UserService userService;
@RequestMapping("/")
public String index(Model model){
return "index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
}

controller包含登录页和首页

现在看登录页的登录form

<form th:action="@{/login}" action="/login" method="post">
<div class="input-group mb-3">
<input type="text" name="username" class="form-control" placeholder="用户名">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input type="password" name="password" class="form-control" placeholder="密码">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-8">
<div class="icheck-primary">
<input type="checkbox" id="remember">
<label for="remember">
Remember Me
</label>
</div>
</div>

<div class="col-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
</div>

</div>
</form>

下面看登录成功首页的退出登录功能

<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary btn-sm" value="注销" />
</form>

当前文章没有包含userService的写法,当然这个可以根据自己的业务来实现。
如果有需要页面权限标签的功能我会单独的文章来展现,这里就不说明了。

动态配置用户角色权限直接的关系需要大家自己来实现了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容