spring boot:spring security用mysql实现动态权限管理(spring boot 2.3.3)


一,动态权限管理的优点和缺点

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)

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM