• springboot-29-security(二)用户角色权限控制


    本博客基于上一个 http://www.cnblogs.com/wenbronk/p/7379865.html

    增加了角色的权限表, 可以进行权限校验

    一, 数据准备

    1, 数据表建立

    /*
    Navicat MySQL Data Transfer
    Source Server         : 本地
    Source Host           : localhost:3306
    Source Database       : test
    Target Server Type    : MYSQL
    Date: 2017-8-14 22:17:33
    */
    
    SET FOREIGN_KEY_CHECKS=0;
    
    -- ----------------------------
    -- Table structure for `sys_user`
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user` (
      `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `username` varchar(32) DEFAULT NULL COMMENT '用户名',
      `password` varchar(32) DEFAULT NULL COMMENT '密码',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Table structure for `sys_role`
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role` (
      `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `name` varchar(32) DEFAULT NULL COMMENT '用户名',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Table structure for `sys_permission`
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_permission`;
    CREATE TABLE `sys_permission` (
      `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `name` varchar(32) DEFAULT NULL COMMENT '用户名',
      `desc` VARCHAR (32) DEFAULT NULL COMMENT '描述',
      `url` VARCHAR (32) DEFAULT NULL COMMENT 'url',
      `pid` INT (32),
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Table structure for `sys_role_user`
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_role_user`;
    CREATE TABLE `sys_role_user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `sys_user_id` INT(32) NOT NULL COMMENT 'user_id',
      `sys_role_id` INT(32) NOT NULL COMMENT 'role_id',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
    
    ALTER TABLE sys_role_user ADD CONSTRAINT sys_FK1 FOREIGN KEY(sys_user_id) REFERENCES sys_user(id);
    ALTER TABLE sys_role_user ADD CONSTRAINT role_FK2 FOREIGN KEY(sys_role_id) REFERENCES sys_role(id);
    
    -- ----------------------------
    -- Table structure for `sys_permission_role`
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_permission_role`;
    CREATE TABLE `sys_permission_role` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
      `sys_role_id` INT(32) NOT NULL COMMENT 'role_id',
      `sys_permission_id` INT(32) NOT NULL COMMENT 'permission_id',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
    ALTER TABLE sys_permission_role ADD CONSTRAINT sys_FK3 FOREIGN KEY(sys_role_id) REFERENCES sys_role(id);
    ALTER TABLE sys_permission_role ADD CONSTRAINT role_FK4 FOREIGN KEY(sys_permission_id) REFERENCES sys_permission(id);

    2, 导入数据

    insert into SYS_USER (id,username, password) values (1,'vini', '123');
    insert into SYS_USER (id,username, password) values (2,'bronk', '123');
    
    insert into SYS_ROLE(id,name) values(1,'ROLE_ADMIN');
    insert into SYS_ROLE(id,name) values(2,'ROLE_USER');
    
    insert into SYS_ROLE_USER(SYS_USER_ID,sys_role_id) values(1,1);
    insert into SYS_ROLE_USER(SYS_USER_ID,sys_role_id) values(2,2);
    
    BEGIN;
    INSERT INTO `sys_permission` VALUES ('1', 'ROLE_HOME', 'home', '/', null),
      ('2', 'ROLE_ADMIN', 'ABel', '/admin', null);
    COMMIT;
    
    BEGIN;
    INSERT INTO `sys_permission_role` VALUES ('1', '1', '1'), ('2', '1', '2'), ('3', '2', '1');
    COMMIT;

    3, mybatis实体, 其余2个和上一篇博客一样

    SysPermission.groovy

    package com.wenbronk.security.entity
    
    /**
     * Created by wenbronk on 2017/8/17.
     */
    class SysPermission {
        int id
        String name
        String desc
        String url
        int pid
    }

    4, application.yml配置服务启动导入

    和上个一样

    二, security部分

    东西比较多, 按流程来, 

    1, WebSecurityConfig.groovy 添加

    http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)

    最终代码为: 

    package com.wenbronk.security.security.config
    
    import com.wenbronk.security.security.interceptor.MyFilterSecurityInterceptor
    import com.wenbronk.security.security.service.CustomUserService
    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.web.access.intercept.FilterSecurityInterceptor
    
    import javax.inject.Inject
    /**
     * Created by wenbronk on 2017/8/15.
     */
    @Configuration
    @EnableWebSecurity
    class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Inject
        CustomUserService customUserService;
        @Autowired
        MyFilterSecurityInterceptor myFilterSecurityInterceptor
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserService);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .anyRequest().authenticated()   // 任何请求都拦截
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .permitAll()        // 登陆后可访问任意页面
                .and()
                .logout().permitAll();  // 注销后任意访问
            http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
        }
    }

    2, 请求会被拦截器拦截, MyFilterSecurityInterceptor, 

    package com.wenbronk.security.security.interceptor
    
    import com.wenbronk.security.security.config.MyAccessDecisonManager
    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.Component
    
    import javax.inject.Inject
    import javax.servlet.*
    
    /**
     * Created by wenbronk on 2017/8/17.
     */
    @Component
    class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    
        @Inject
        FilterInvocationSecurityMetadataSource securityMetadataSource
    
        @Autowired
        public void setMyAccessDecisionManager(MyAccessDecisonManager myAccessDecisonManager) {
            super.setAccessDecisionManager(myAccessDecisonManager)
        }
    
        @Override
        void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        /**
         * fi中有一个被拦截的url
         * 里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
         //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
         */
        @Override
        void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(request, response, chain)
            invoke(fi)
        }
    
        void invoke(FilterInvocation filterInvocation) {
            InterceptorStatusToken token = super.beforeInvocation(filterInvocation)
            try {
                filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse())
            } finally {
                super.afterInvocation(token, null)
            }
        }
    
        @Override
        void destroy() {
    
        }
    
        @Override
        Class<?> getSecureObjectClass() {
            return FilterInvocation.class
        }
    
        @Override
        SecurityMetadataSource obtainSecurityMetadataSource() {
            return this.securityMetadataSource
        }
    }

    3, 拦截器会调用 MyInvocationSecurityMetaDataSource的 getAttribute方法 获取 filter的权限, 并且滴哦啊用 MyAccessDecisionManager的 decide 方法来校验是否拥有权限

    MyInvocationSecurityMetadataSource.groovy

    package com.wenbronk.security.security.service
    
    import com.wenbronk.security.entity.SysPermission
    import com.wenbronk.security.mapper.SysPermissionMapper
    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.security.web.util.matcher.AntPathRequestMatcher
    import org.springframework.stereotype.Service
    
    import javax.inject.Inject
    import javax.servlet.http.HttpServletRequest
    
    /**
     * Created by wenbronk on 2017/8/17.
     */
    @Service
    class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource{
    
        @Inject
        SysPermissionMapper sysPermissionMapper
    
        /**
         * 此方法是为了判定用户请求的url 是否在权限表中,
         * 如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
         */
        @Override
        Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
            Map<String, Collection<ConfigAttribute>> map = loadResourceDefine()
            HttpServletRequest request = ((FilterInvocation) o).getHttpRequest()
            map.each {entry ->
                AntPathRequestMatcher matcher = new AntPathRequestMatcher(entry.getKey())
                if (matcher.matches(request)) {
                    // 匹配, 返回给decide方法
                    return entry.getValue()
                }
            }
            return null
        }
    
        /**
         * 加载权限表中所有的权限
         */
        public Map<String, Collection<ConfigAttribute>> loadResourceDefine() {
            Map<String, Collection<ConfigAttribute>> map = new HashMap<>()
            List<SysPermission> permissions = sysPermissionMapper.findAll()
            permissions.each {permission ->
                List<ConfigAttribute> array = new ArrayList<>()
                // 此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。
                // 此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
                ConfigAttribute cfg = new SecurityConfig(permission.getName())
                array.add(cfg)
                // 用权限的url作为key, 权限的名称集合为value
                map.put(permission.getUrl(), array);
            }
    
            return null
        }
    
        @Override
        Collection<ConfigAttribute> getAllConfigAttributes() {
            return null
        }
    
        @Override
        boolean supports(Class<?> aClass) {
            return true
        }
    }

    4, MyInvocationSecurityMetadataSource 查询数据库中url和所需权限的关系, 返回给 MyAccessDecisionManger

    MyAccessDecisionManger.groovy

    package com.wenbronk.security.security.config
    
    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.stereotype.Service
    
    /**
     * Created by wenbronk on 2017/8/17.
     */
    @Service
    class MyAccessDecisonManager implements AccessDecisionManager {
    
        /**
         * 判断是否拥有权限的决策方法
         * authentication 是CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
         * object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
         * configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,
         */
        @Override
        void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            if (null == configAttributes || configAttributes.size() <=0 )
                return
            configAttributes.each {configAttribute ->
                String needRole = configAttribute.getAttribute()
                authentication.getAuthorities().each {authority ->
                    if (needRole.trim().equals(authority.getAuthority()))
                        return
                }
            }
            throw new java.nio.file.AccessDeniedException('no right')
        }
    
        @Override
        boolean supports(ConfigAttribute configAttribute) {
            return true
        }
    
        @Override
        boolean supports(Class<?> aClass) {
            return true
        }
    }

    decide进行是否有权限的决策, 正常则返回, 没有就抛出异常

    5, 最终在 customuserService进行权限和用户的装配, 准备返回给前端

    package com.wenbronk.security.security.service
    
    import com.wenbronk.security.entity.SysPermission
    import com.wenbronk.security.entity.SysUser
    import com.wenbronk.security.mapper.SysPermissionMapper
    import com.wenbronk.security.mapper.SysUserMapper
    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 javax.inject.Inject
    /**
     * Created by wenbronk on 2017/8/15.
     */
    @Service
    class CustomUserService implements UserDetailsService {
    
        @Inject
        SysUserMapper sysUserMapper
        @Inject
        SysPermissionMapper sysPermissionMapper
    
        @Override
        UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            def sysUser = sysUserMapper.findByUserName(s) as SysUser
            if (sysUser != null) {
                List<SysPermission> permissions = sysPermissionMapper.findByAdminUserId(sysUser.getId())
                List<GrantedAuthority> grantedAuthorities = new ArrayList<>()
                permissions.each {permission ->
                    if (permission != null && permission.getName() != null) {
                        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName())
                        // 此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                        grantedAuthorities.add(grantedAuthority)
                    }
                }
                return new User(sysUser.getUsername(), sysUser.getPassword(), grantedAuthorities)
            }else {
                throw new UsernameNotFoundException('admin: ' + s + ' do not exits')
            }
        }
    }

    三: 页面部分

    1, 页面转向config, 和上一篇的一样

    @Configuration
    class WebMvcConfig extends WebMvcConfigurerAdapter{
    
        @Override
        void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login")
    //        registry.addViewController("/home").setViewName("home")
        }
    }

    2, 更改home.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
    <head>
    <meta content="text/html;charset=UTF-8"/>
    <title sec:authentication="name"></title>
    <link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
    <style type="text/css">
    body {
      padding-top: 50px;
    }
    .starter-template {
      padding: 40px 15px;
      text-align: center;
    }
    </style>
    </head>
    <body>
         <nav class="navbar navbar-inverse navbar-fixed-top">
          <div class="container">
            <div class="navbar-header">
              <a class="navbar-brand" href="#">Spring Security演示</a>
            </div>
            <div id="navbar" class="collapse navbar-collapse">
              <ul class="nav navbar-nav">
               <li><a th:href="@{/}"> 首页 </a></li>
                  <li><a th:href="@{/admin}"> admin </a></li>
              </ul>
            </div><!--/.nav-collapse -->
          </div>
        </nav>
    
    
         <div class="container">
    
          <div class="starter-template">
              <h1 th:text="${msg.title}"></h1>
    
            <p class="bg-primary" th:text="${msg.content}"></p>
    
            <div sec:authorize="hasRole('ROLE_HOME')"> <!-- 用户类型为ROLE_ADMIN 显示 -->
                 <p class="bg-info" th:text="${msg.etraInfo}"></p>
            </div>
    
            <div sec:authorize="hasRole('ROLE_ADMIN')"> <!-- 用户类型为 ROLE_USER 显示 -->
                 <p class="bg-info">恭喜, 有ROLE_ADMIN的权限</p>
            </div>
    
            <form th:action="@{/logout}" method="post">
                <input type="submit" class="btn btn-primary" value="注销"/>
            </form>
          </div>
    
        </div>
    </body>
    
    
    </html>

    四, 密码进行加密

    注意, 如果使用, 存储进数据库的密码也必须是同一个算法计算的密文

    md5, 工具类

    package com.wenbronk.security.tools
    
    import java.security.MessageDigest
    
    /**
     * Created by wenbronk on 2017/8/17.
     */
    class MD5Utils {
        private static final String SALT = "tamboo";
    
        public static String encode(String password) {
            password = password + SALT;
            MessageDigest md5 = null;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            char[] charArray = password.toCharArray();
            byte[] byteArray = new byte[charArray.length];
    
            for (int i = 0; i < charArray.length; i++)
                byteArray[i] = (byte) charArray[i];
            byte[] md5Bytes = md5.digest(byteArray);
            StringBuffer hexValue = new StringBuffer();
            for (int i = 0; i < md5Bytes.length; i++) {
                int val = ((int) md5Bytes[i]) & 0xff;
                if (val < 16) {
                    hexValue.append("0");
                }
    
                hexValue.append(Integer.toHexString(val));
            }
            return hexValue.toString();
        }
    
        public static void main(String[] args) {
            System.out.println(MD5Utils.encode("abel"));
        }
    }

    2, 修改 webSecurityConfig的加密方法

    @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(customUserService()).passwordEncoder(
    new PasswordEncoder(){ @Override public String encode(CharSequence rawPassword) { return MD5Util.encode((String)rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5Util.encode((String)rawPassword)); }}); //user Details Service验证 }

     BCrypt的强hash算法

     BCrypt强哈希方法 每次加密的结果都不一样。但是存贮其中一次加密结果 也能够验证成功

    1, 修改WebSecurityConfig

        @Autowired
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserService).passwordEncoder(new BCryptPasswordEncoder());
        }

    2, 进行加密

      public SysUser create(User u user){
            //进行加密
            BCryptPasswordEncoder encoder =new BCryptPasswordEncoder();
            sysUser.setPassword(encoder.encode(user.getRawPassword().trim()));
            userDao.create(user);
        return sysUser;

    原博客地址: 

    http://blog.csdn.net/u012373815/article/details/54633046

    http://blog.csdn.net/u012373815/article/details/60465776

  • 相关阅读:
    从Kratos设计看Go微服务工程实践
    京东到家安全测试实践
    浅谈 Protobuf 编码 原创 gsonli 腾讯技术工程 2021-07-14
    API Design Guide
    The power of two choices in randomized load balancing
    NGINX and the "Power of Two Choices" Load-Balancing Algorithm
    SRE 崩溃
    DDoS木马
    String.fromCharCode(88,83,83) 方法返回由指定的 UTF-16 代码单元序列创建的字符串
    汇编语言的AX,BX,CX,DX,分别表示什么
  • 原文地址:https://www.cnblogs.com/wenbronk/p/7381252.html
Copyright © 2020-2023  润新知