栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

security+sm2实现权限管理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

security+sm2实现权限管理

最近接触到了国密算法,稍微做了些了解,打算实际应用一下。正好之前权限管理使用的shiro,security没有从头到尾搞过,就打算做一套security+sm2实现自定义token校验登录的东西。

思路是这样的:先搞一套security,不使用jwt,后面再把sm2集成进去,使用redis缓存token进行校验。

第一次写博客,如果乱的话请见谅。

导入security的jar包

5.6.0


   org.springframework.security
   spring-security-web
   ${spring.security.version}


   org.springframework.security
   spring-security-config
   ${spring.security.version}

security基础配置直接参考下面这位,只是有些地方做了下改动。Springboot + Spring Security 实现前后端分离登录认证及权限控制_I_am_Hutengfei的博客-CSDN博客_springboot springsecurity 前后端分离

 自定义数据库用户表

用户信息封装类:

import com.zzh.model.SysPermission;
import com.zzh.model.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;


public class UserDetail implements UserDetails {

    private SysUser user;

    private List permissions;

    public UserDetail(SysUser user, List permissions){
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection getAuthorities() {
        return permissions.stream().map(sysPermission -> new SimpleGrantedAuthority(sysPermission.getPermissionCode())).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassWord();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

 因为我们是自定义的用户表,没有账号过期、锁定等相关的配置,所以UserDetail中的isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired()、isEnabled()全部改为true。权限则继承security的UserDetails后进行重写,将我们查出来的权限列表放进去。密码返回我们自己定义的密码(注意UserDetails中password是小写,坑了我一下)。

WebSecurityConfig中添加.and().csrf().disable()关闭csrf防护,否则post请求会被拦截。

.and().csrf().disable()

集成swagger


            com.github.xiaoymin
            knife4j-spring-boot-starter
            2.0.7
        

添加swagger配置

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {

    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        Docket docket=new Docket(documentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .description("# swagger-bootstrap-ui RESTful APIs")
                        .termsOfServiceUrl("http://www.xx.com/")
                        .contact("xxxx.com")
                        .version("1.0")
                        .build())
                //分组名称
                .groupName("2.X版本")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.zzh.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

}

swagger访问地址:http://127.0.0.1:8080/doc.html

集成mybatis-plus-generator,自动生成mybatis代码

3.4.1


            com.baomidou
            mybatis-plus-generator
            ${mybatis.plus.version}
        
public class MybatisGenerator {

    static final ResourceBundle resourceBundle = ResourceBundle.getBundle("mybatis-plus");

    public MybatisGenerator() {
    }

    public static void main(String[] args) {
        codeGenerate(false, false, false, true, false);
    }

    public static void codeGenerate(boolean createController, boolean createService, boolean createServiceImpl, boolean createEntity, boolean createMapper) {
        AutoGenerator autoGenerator = new AutoGenerator();
        GlobalConfig gc = new GlobalConfig();
        String pjPath = resourceBundle.getString("projectPath");
        if (StringUtils.isBlank(pjPath)) {
            pjPath = System.getProperty("user.dir");
        }
        final String projectPath = pjPath;
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor(resourceBundle.getString("author"));
        gc.setOpen(false);
        gc.setSwagger2(true);
        gc.setbaseResultMap(true);
        gc.setIdType(IdType.ASSIGN_ID);
        autoGenerator.setGlobalConfig(gc);
        String dbType = resourceBundle.getString("dbType");
        String schemaName = resourceBundle.getString("schemaName");
        DataSourceConfig dsc = new DataSourceConfig();
        if (StringUtils.isNotBlank(dbType)) {
            dsc.setDbType(DbType.getDbType(dbType));
        }

        if (StringUtils.isNotBlank(schemaName)) {
            dsc.setSchemaName(schemaName);
        }

        dsc.setDriverName(resourceBundle.getString("driverName"));
        dsc.setUrl(resourceBundle.getString("url"));
        dsc.setUsername(resourceBundle.getString("userName"));
        dsc.setPassword(resourceBundle.getString("password"));
        dsc.setTypeConvert(new MybatisGenerator.MySqlTypeConvertCustom());
        autoGenerator.setDataSource(dsc);
        final String packageName = resourceBundle.getString("parent");
        PackageConfig pc = new PackageConfig();
        pc.setParent(packageName);
        pc.setService("service");
        pc.setServiceImpl("service.impl");
        pc.setEntity("model");
        pc.setMapper("mapper");
        autoGenerator.setPackageInfo(pc);
        InjectionConfig cfg = new InjectionConfig() {
            public void initMap() {
            }
        };
        String templatePath = "/templates/mapper.xml.vm";
        List focList = new ArrayList();
        focList.add(new FileOutConfig(templatePath) {
            public String outputFile(TableInfo tableInfo) {
                return projectPath + "/src/main/resources/mapper//" + tableInfo.getEntityName() + "Mapper" + ".xml";
            }
        });
        focList.add(new FileOutConfig("/templates/entityVO") {
            public String outputFile(TableInfo tableInfo) {
                return projectPath + "/src/main/java/" + packageName.replace(".", "/") + "/vo/" + tableInfo.getEntityName() + "VO" + ".java";
            }
        });
        focList.add(new FileOutConfig("/templates/entityDTO") {
            public String outputFile(TableInfo tableInfo) {
                return projectPath + "/src/main/java/" + packageName.replace(".", "/") + "/dto/" + tableInfo.getEntityName() + "DTO" + ".java";
            }
        });
        cfg.setFileOutConfigList(focList);
        autoGenerator.setCfg(cfg);
        TemplateConfig templateConfig = new TemplateConfig();
        if (!createController) {
            templateConfig.setController(null);
        }

        if (!createService) {
            templateConfig.setService(null);
        }

        if (!createServiceImpl) {
            templateConfig.setServiceImpl(null);
        }

        if (!createEntity) {
            templateConfig.setEntity(null);
        }

        if (!createMapper) {
            templateConfig.setMapper(null);
        }

        templateConfig.setXml(null);
        autoGenerator.setTemplate(templateConfig);
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setInclude(resourceBundle.getString("tableNames").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix(new String[]{pc.getModuleName() + "_"});
        autoGenerator.setStrategy(strategy);
        autoGenerator.execute();
    }

    static class MySqlTypeConvertCustom extends MySqlTypeConvert {
        MySqlTypeConvertCustom() {
        }

        public IColumnType processTypeConvert(GlobalConfig globalConfig, String fieldType) {
            String t = fieldType.toLowerCase();
            return t.contains("tinyint(1)") ? DbColumnType.INTEGER : super.processTypeConvert(globalConfig, fieldType);
        }
    }

}
    public static void main(String[] args) {
        MybatisGenerator.codeGenerate(false,false,false,true,true);
    }

执行main方法,自动读取mybatis-plus配置文件生成相关代码

到此为止,mybatis-plus+security+swagger已经配置完成,接下来开始自定义token进行访问校验。

因为之前没用过SM2,所以也是到处找资料,感谢SM2的非对称加解密java工具类 - 吃奶滴虫虫 - 博客园

说一下上面那篇的两个错误:

1、原文printHexString方法中打印结果的时候,builder.append('0'+hex);多追加了一个0,此处应该去掉。

2、原本打印密文的时候打印了两遍。

在此基础上对SM2的工具类进行优化,将密钥保存进文件中:
 

    public SM2KeyPair generateKeyPair() {
        BigInteger d = random(n.subtract(new BigInteger("1")));
        SM2KeyPair keyPair = new SM2KeyPair(G.multiply(d).normalize(), d);
        if (checkPublicKey(keyPair.getPublicKey())) {
            exportPrivateKey(keyPair.getPrivateKey());
            exportPublicKey(keyPair.getPublicKey());
            log.info("generate key successfully");
            return keyPair;
        } else {
            log.info("generate key failed");
        }
        return null;
    }

    public SM2Util() {
        curve = new ECCurve.Fp(p, // q
                a, // a
                b); // b
        G = curve.createPoint(xg, yg);
    }

    
    public void exportPrivateKey(BigInteger privateKey) {
        File file = new File("prk");
        try {
            if (!file.exists())
                file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(privateKey);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    
    public void exportPublicKey(ECPoint publicKey) {
        File file = new File("puk");
        try {
            if (!file.exists())
                file.createNewFile();
            byte buffer[] = publicKey.getEncoded(false);
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(buffer);
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public BigInteger importPrivateKey() {
        File file = new File("prk");
        try {
            if (!file.exists())
                return null;
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            BigInteger res = (BigInteger) (ois.readObject());
            ois.close();
            fis.close();
            return res;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    public ECPoint importPublicKey() {
        File file = new File("puk");
        try {
            if (!file.exists())
                return null;
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            byte buffer[] = new byte[16];
            int size;
            while ((size = fis.read(buffer)) != -1) {
                baos.write(buffer, 0, size);
            }
            fis.close();
            return curve.decodePoint(baos.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

 将解密后的字节数组转为字符串

    public static byte[] hexToByte(String hex) throws IllegalArgumentException {
        if (hex.length() % 2 != 0) {
            throw new IllegalArgumentException();
        }
        if (hex.length() < 1) {
            return null;
        } else {
            byte[] result = new byte[hex.length() / 2];
            int j = 0;
            for(int i = 0; i < hex.length(); i+=2) {
                result[j++] = (byte)Integer.parseInt(hex.substring(i,i+2), 16);
            }
            return result;
        }
    }

使用main方法测试成功

 public static void main(String[] args){
        String aaa = "123456";
        SM2Util sm2 = new SM2Util();
        String data = sm2.encrypt(aaa);
        System.out.println("密文n"+data);
        System.out.println(sm2.decrypt(data));
    }

自定义权限过滤器,继承OncePerRequestFilter,从header中取到token进行校验是否合法

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//        接口是否在白名单中,如果在白名单则不校验token,不登录
        String uri = request.getRequestURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : permitConfig.getUrls()) {
            if(pathMatcher.match(path, uri)){
                filterChain.doFilter(request, response);
                return;
            }
        }
        String authHeader = request.getHeader("token");
        if(StringUtils.isNotBlank(authHeader)){
            UserDetail userDetail = sm2Util.decryptToUser(authHeader);
            if(userDetail != null){
                String token = redisUtil.get(userDetail.getUsername());
                if(!StringUtils.isBlank(token)){
                    if(StringUtils.equals(token, authHeader)){
                        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
                        SecurityContextHolder.getContext().setAuthentication(authenticationToken); //登录状态
                    }
                }
            }
        }
        filterChain.doFilter(request, response);
    }

修改WebSecurityConfig,移除旧的登录校验,添加新的权限过滤

http.addFilterBefore(authenticationTokenFilter, FilterSecurityInterceptor.class);

测试整个项目:

登录:

 返回token,拿到token放入header去请求其他接口:

有权限的可以请求成功,无权限的会提示无权限 。

其他:

1、WebSecurityConfig中要加.and().csrf().disable(),否则post请求会失败,因为默认开启了csrf跨域拦截
2、security中登录校验密码是password,注意小写。我是自定义表结构pass_word,驼式转换后是passWord,所以在自定义UserDetail类中返回password的时候return user.getPassWord()
3、自定义表中没有账号过期、锁定等相关的配置,所以UserDetail中的isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired()、isEnabled()全部改为true。如果后期需要添加锁定相关的话,可以直接return 条件。
4、自定义登录,CustomizeAuthenticationSuccessHandler不需要了,只用在自己的登录接口中处理登录成功后的逻辑
5、WebSecurityConfig中http.addFilterBefore,将自定义过滤器替换原本的登录成功
6、不需要校验登录token的API请求直接在yml中permit里添加,否则每次请求都走解密校验会有性能损耗
7、yml中permit里配置了API白名单后,数据库中权限里不要再配该API,否则权限过滤会被拦截,因为没有登录

完整版代码地址:Gitee

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/666500.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号