一,動態權限管理的優點和缺點
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)