最近接触到了国密算法,稍微做了些了解,打算实际应用一下。正好之前权限管理使用的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 extends GrantedAuthority> 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-starter2.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



