本博客基于上一个增加了角色的权限表, 可以进行权限校验

一, 数据准备

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;