原来项目一直使用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的写法,当然这个可以根据自己的业务来实现。
如果有需要页面权限标签的功能我会单独的文章来展现,这里就不说明了。
动态配置用户角色权限直接的关系需要大家自己来实现了。
网友评论