一,动态权限管理的优点和缺点
1,优点:
因为控制权限的数据保存在了mysql或其他存储系统中,
可以动态修改权限控制,无需改动代码和重启应用,
权限变更时灵活方便
2,缺点:
权限的设置需要保存在外部存储系统,
每次request时都需要查库处理,
高并发时影响效率
说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔 邮箱: 371125307@qq.com
二,演示项目的相关信息
1,项目地址:
https://github.com/liuhongdi/securitydynamic
2,项目功能说明
通过修改mysql数据库中的数据,
实现对权限验证的动态控制,无需修改代码和重启应用
3,项目结构:如图:
三,配置文件说明
1,pom.xml
<!--security begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--thymeleaf begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--validation begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!--mysql mybatis begin--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- JSON解析fastjson begin--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency>
2,application.properties:
#thymeleaf spring.thymeleaf.cache=false spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.mode=HTML spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html #mysql spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false spring.datasource.username=root spring.datasource.password=lhddemo spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis mybatis.mapper-locations=classpath:/mapper/*Mapper.xml mybatis.type-aliases-package=com.example.demo.mapper #error server.error.include-stacktrace=always #log logging.level.org.springframework.web=trace logging.level.org.springframework.security=debug
3,数据库:
建立各个表的sql:
CREATE TABLE `sys_user` ( `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码', `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称', PRIMARY KEY (`userId`), UNIQUE KEY `userName` (`userName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'
INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老刘'), (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理员'), (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商户老张');
CREATE TABLE `sys_user_role` ( `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用户id', `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id', PRIMARY KEY (`urId`), UNIQUE KEY `userId` (`userId`,`roleName`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表'
INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES (1, 2, 'ADMIN'), (2, 3, 'MERCHANT');
CREATE TABLE `sys_menu` ( `menuId` int(11) NOT NULL AUTO_INCREMENT, `pattern` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, PRIMARY KEY (`menuId`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='method'
INSERT INTO `sys_menu` (`menuId`, `pattern`) VALUES (1, '/home/**'), (2, '/login/**'), (3, '/js/**'), (4, '/admin/**'), (5, '/merchant/**');
CREATE TABLE `sys_menu_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `menuId` int(11) DEFAULT NULL, `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT 'role名字', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='menu角色对应表'
INSERT INTO `sys_menu_role` (`id`, `menuId`, `roleName`) VALUES (1, 1, 'ALL'), (2, 2, 'ALL'), (3, 3, 'ALL'), (4, 4, 'ADMIN'), (5, 5, 'MERCHANT'), (6, 5, 'ADMIN');
说明:sys_user表中,3个用户的密码都是111111,仅供演示使用,大家在生产环境中一定不要这样设置
四,java代码说明
1,WebSecurityConfig.java
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final static BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(); @Resource private UserLoginFailureHandler userLoginFailureHandler;//登录失败的处理类 @Resource private UserLoginSuccessHandler userLoginSuccessHandler;//登录成功的处理类 @Resource private UserLogoutSuccessHandler userLogoutSuccessHandler;//退出成功的处理类 @Resource private UserAccessDeniedHandler userAccessDeniedHandler;//无权访问的处理类 @Resource private SecUserDetailService secUserDetailService; //用户信息类,用来得到UserDetails @Resource private CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource; @Resource private CustomAccessDecisionManager customAccessDecisionManager; //指定加密的方式,避免出现:There is no PasswordEncoder mapped for the id "null" @Bean public PasswordEncoder passwordEncoder(){//密码加密类 return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //通过数据库的配置,动态判断当前用户是否可以访问当前url http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource); object.setAccessDecisionManager(customAccessDecisionManager); return object; } }); //login http.formLogin() .loginPage("/login/login") .loginProcessingUrl("/login/logined")//发送Ajax请求的路径 .usernameParameter("username")//请求验证参数 .passwordParameter("password")//请求验证参数 .failureHandler(userLoginFailureHandler)//验证失败处理 .successHandler(userLoginSuccessHandler)//验证成功处理 .permitAll(); //登录页面用户任意访问 //logout http.logout() .logoutUrl("/login/logout") .logoutSuccessUrl("/login/logout") .logoutSuccessHandler(userLogoutSuccessHandler)//登出处理 .deleteCookies("JSESSIONID") .clearAuthentication(true) .invalidateHttpSession(true) .permitAll(); //其他任何请求,登录后可以访问 http.authorizeRequests().anyRequest().authenticated(); //accessdenied http.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler);//无权限时的处理 //user detail http.userDetailsService(secUserDetailService); } @Resource public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(secUserDetailService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return ENCODER.encode(charSequence); } //密码匹配,看输入的密码经过加密与数据库中存放的是否一样 @Override public boolean matches(CharSequence charSequence, String s) { return ENCODER.matches(charSequence,s); } }); } }
2,CustomAccessDecisionManager.java
@Component public class CustomAccessDecisionManager implements AccessDecisionManager { //比较用户的权限和url所需要的权限,确定是否可以访问 @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { for (ConfigAttribute configAttribute : configAttributes) { //如果是all,表示所有的都允许访问 if ("ALL".equals(configAttribute.getAttribute())) { return; } //是否没有权限要求 if ("ROLE_def".equals(configAttribute.getAttribute())) { if (authentication instanceof AnonymousAuthenticationToken) { System.out.println("匿名用户"); throw new AccessDeniedException("权限不足,无法访问!"); } else { System.out.println("其他类型用户,可以访问"); return; } } Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { String userRole = authority.getAuthority(); //数据库中没有保存ROLE_,这里添加上 String menuRole = "ROLE_"+configAttribute.getAttribute(); if (userRole.equals(menuRole)) { //System.out.println("进入应用系统"); return; } } } throw new AccessDeniedException("权限不足,无法访问!"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
用来判断当前用户是否有权限访问当前的url
3,CustomFilterInvocationSecurityMetadataSource.java
@Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { AntPathMatcher pathMatcher = new AntPathMatcher(); @Autowired private SysMenuService sysMenuService; //得到所有的menu, //查询出匹配当前url的所有role @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) object).getRequestUrl(); List<SysMenu> menus = sysMenuService.getMenus(); for (SysMenu menu : menus) { if (pathMatcher.match(menu.getPattern(), requestUrl)) { List<String> roles = menu.getRoles(); String[] roleStr = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { roleStr[i] = roles.get(i); } return SecurityConfig.createList(roleStr); } } return SecurityConfig.createList("ROLE_def"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
通过查询数据库得到匹配当前url的role
4,SecUser.java
public class SecUser extends User {
//用户id
private int userid;
//用户昵称
private String nickname;
public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
}
spring security中User类的子类,增加了用户id和昵称,
需要保存到session中的信息,在这里扩展
目的是避免在每个页面上显示用户信息需要查数据库
5,SecUserDetailService.java
/**
* Created by liuhongdi on 2020/07/09.
*/
@Component("SecUserDetailService")
public class SecUserDetailService implements UserDetailsService{
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//查库
SysUser oneUser = sysUserService.getOneUserByUsername(s);//数据库查询 看用户是否存在
String encodedPassword = oneUser.getPassword();
Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合
//用户权限:需要加 ROLE_
List<String> roles = oneUser.getRoles();
//System.out.println(roles);
for (String roleone : roles) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone);
collection.add(grantedAuthority);
}
//增加用户的userid,nickname
SecUser user = new SecUser(s,encodedPassword,collection);
user.setUserid(oneUser.getUserId());
user.setNickname(oneUser.getNickName());
return user;
}
}
6,UserAccessDeniedHandler.java
@Component("UserAccessDeniedHandler")
public class UserAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AccessDeniedException e) throws IOException, ServletException {
boolean isAjax = ServletUtil.isAjax();
//System.out.println("isajax:"+isAjax);
if (isAjax == true) {
ServletUtil.printRestResult(RestResult.error(ResponseCode.ACCESS_DENIED));
} else {
ServletUtil.printString(ResponseCode.ACCESS_DENIED.getMsg());
}
}
}
7,UserLoginFailureHandler.java
@Component("UserLoginFailureHandler")
public class UserLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//System.out.println("UserLoginFailureHandler");
ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL));
}
}
8,UserLoginSuccessHandler.java
@Component("UserLoginSuccessHandler")
public class UserLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//System.out.println("UserLoginSuccessHandler");
ServletUtil.printRestResult(RestResult.success(0,"登录成功"));
}
}
9,UserLogoutSuccessHandler.java
@Component("UserLogoutSuccessHandler")
public class UserLogoutSuccessHandler implements LogoutSuccessHandler{
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletRequest.getSession().invalidate();
ServletUtil.printRestResult(RestResult.success(0,"退出成功"));
}
}
10,WebInterceptor.java
@Component
public class WebInterceptor extends HandlerInterceptorAdapter {
//如果view不为空,把登录信息传递给模板
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
if (modelAndView != null) {
ModelMap modelMap = modelAndView.getModelMap();
SecUser currentUser = SessionUtil.getCurrentUser();
if (currentUser != null) {
modelMap.addAttribute("is_login","1");
modelMap.addAttribute("login_username",currentUser.getNickname());
} else {
modelMap.addAttribute("is_login","0");
modelMap.addAttribute("login_username","");
}
}
}
}
负责把传递页面公共部分显示的数据到模板
11,login.html
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=UTF-8"/>
<title>登录页面</title>
<script type="text/javascript" language="JavaScript" src="/js/jquery-1.6.2.min.js"></script>
<style type="text/css">
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
</style>
<!-- CSRF -->
<meta name="_csrf" th:content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="/home/home"> 首页 </a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<h2>使用账号密码登录</h2>
<div class="form-group">
<label for="username">账号</label>
<input type="text" class="form-control" id="username" name="username" value="" placeholder="账号" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password" placeholder="密码" />
</div>
<button name="formsubmit" value="登录" onclick="go_login()" >登录</button>
</div>
</div>
<script>
function go_login(){
if ($("#username").val() == "") {
alert('用户名不可为空');
$("#username").focus();
return false;
}
if ($("#password").val() == "") {
alert('密码不可为空');
$("#password").focus();
return false;
}
var postdata = {
username:$("#username").val(),
password:$("#password").val(),
}
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");
$.ajax({
type:"POST",
//type:"GET",
url:"/login/logined",
data:postdata,
//返回数据的格式
datatype: "json",//"xml", "html", "script", "json", "jsonp", "text".
beforeSend: function(request) {
request.setRequestHeader(csrfHeader, csrfToken); // 添加 CSRF Token
},
success:function(data){
if (data.code == 0) {
alert('login success:'+data.msg);
window.location.href="/home/home";
} else {
alert("failed:"+data.msg);
}
},
//调用执行后调用的函数
complete: function(XMLHttpRequest, textStatus){
},
//调用出错执行的函数
error: function(){
//请求出错处理
alert('error');
}
});
}
</script>
</body>
</html>
12,页面上用到的其他代码,可以移步github.com上查看
五,测试效果
1,访问首页:
http://127.0.0.1:8080/home/home
未登录时:
2,以普通用户lhd登录:
访问:管理员首页/商户首页,都会得到提示
无权访问
访问修改密码 页面,可以访问
3,以merchant用户登录:
role是MERCHANT
访问:管理员首页,提示:
无权访问
访问商户首页:可以访问
访问修改密码 页面,可以访问
4,以admin用户登录:
role是ADMIN
访问管理员首页:可以访问
访问商户首页:可以访问
访问修改密码 页面,可以访问
5,从mysql数据库sys_menu_role表中删除:meuid=5 rolename=ADMIN
这条记录,
则用admin登录后也不能再访问商户首页,
无权访问
无需重启应用
六,查看spring boot的版本:
. ____ _ __ _ _ /\ / ___'_ __ _ _(_)_ __ __ _ ( ( )\___ | '_ | '_| | '_ / _` | \/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.3.RELEASE)