山海Power

山海Power - 基于SpringBoot的通用Web权限组件

ShanHaiPower-based SpringBoot Web Permission components

GitHub release (latest by date) GitHub closed issues GitHub top language
GitHub Code Size GitHub Code Lines GitHub License

# 1. 组件能力

  • 支持用户进行互斥鉴权

  • 基于注解模式的权限校验

  • 基于路由模式的权限校验

  • 支持前后端分离模式下独立用户鉴权和会话数据读写

  • 支持自定义权限集合实现和自定义路由集合实现

  • 支持自行扩展缓存协议实现数据持久化(组件默认提供基于原生Redis协议的持久化组件)

  • 集成OTP生成与校验核心模块,可以自行实现OTP绑定与校验能力

# 1.1 引入组件

<dependency>
    <groupId>com.wangshanhai.power</groupId>
    <artifactId>shanhai-power-spring-boot-starter</artifactId>
    <version>${last.version}</version>
</dependency>

# 1.2 启用组件

SpringBoot 2.x 启用方式

@Configuration
@EnableShanHaiPower
public class ShanhaiConfig implements WebMvcConfigurer {
  
}

SpringBoot 1.5.x 启用方式

1)添加如下配置:

shanhai:
  power:
    auto-regist: false #低版本需要进行手动注册  

2)自行完成对应组件注册,示例如下:

@Configuration
@EnableShanHaiPower
@EnableConfigurationProperties(ShanhaiPowerConfig.class)
@AutoConfigureAfter(WebMvcConfigurationSupport.class)
public class ShanhaiConfig extends WebMvcConfigurationSupport {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ShanhaiPowerInterceptor()).addPathPatterns("/**");
        //按需启用(注解权限)
        //registry.addInterceptor(new ShanhaiPowerAnnotationPermissionsInterceptor()).addPathPatterns("/**");
        //按需启用(路由权限)
        //registry.addInterceptor(new ShanhaiPowerRoutePermissionsInterceptor()).addPathPatterns("/**");
    }
}

3)组件说明:

组件 组件名称 组件说明
ShanhaiPowerInterceptor 用户身份鉴权组件 对用户登录的有效性进行鉴权
ShanhaiPowerAnnotationPermissionsInterceptor 用户注解权限鉴权组件 对用户进行单一资源有效性进行鉴权
ShanhaiPowerRoutePermissionsInterceptor 用户路由权限鉴权组件 对用户进行路由资源有效性进行鉴权

# 1.3 配置说明

shanhai:
  power:
    tokenName: token #Token名称
    tokenAlgorithm: uuid # Token生成算法
    token-prefix: 'shanhai ' #Token前缀
    exclusive-login: true #同端互斥登录 (默认为false)
    auth-path-patterns: #鉴权组件拦截范围
       - /**
    auth-exclude-path-patterns:  #鉴权组件拦截白名单范围 
       - /api/xxx

# 1.4 用户会话组件

API说明 API能力 备注
ShanhaiPower.login("xxx") 通过用户标识进行登录(xxx为用户名或ID) 1.默认渠道为Default 2.返回TokenInfo
ShanhaiPower.login("xxx","PC") 指定渠道登录 返回TokenInfo
ShanhaiPower.getCurrentUserToken() 获取当前登录的用户信息 返回TokenInfo
ShanhaiPower.setTokenSessionData(String key,Object data) 设置基于Token的会话级数据
ShanhaiPower.getTokenSessionData(String key) 获取基于Token的会话级数据
ShanhaiPower.logOutByToken(String token) 注销指定Token
ShanhaiPower.logOut(Object userFlag) 注销指定用户会话
ShanhaiPower.logOut(Object userFlag,String channel) 注销指定用户指定渠道会话

忽略用户会话校验

@RequestNotNeedAuth
@GetMapping("/login")
public TokenInfo login(){
   return ShanhaiPower.login("xxx");
}

# 1.5 注解权限组件

注解权限组件配置参数如下:

shanhai:
  power:
    annotation-permissions-enable: true #启用路由权限组件
    permission-path-patterns: #权限组件拦截范围 (路由和注解权限共享)
      - /**
    permission-exclude-path-patterns: #权限组件拦截白名单范围 (路由和注解权限共享)
      - /api/xxx

注解权限组件示例如下:

@RequiresPermissions("user:details")
@GetMapping("/queryUserInfo")
public TokenInfo queryUserInfo(){
    return ShanhaiPower.getCurrentUserToken();
}

在对应的方法上添加@RequiresPermissions,并且写入权限编码。

实现PermissionService,动态为当前用户追加权限编码。

@Service
public class PermissionServiceImpl implements PermissionService {
    @Override
    public List<String> queryAllPermission(HttpServletRequest request) {
        List<String> allPermission=new ArrayList<>();
        allPermission.add("user:details");
        allPermission.add("user:route");
        return allPermission;
    }

}

# 1.6 路由权限组件

路由权限组件配置参数如下:

shanhai:
  power:
    route-permission-enable: true #启用路由权限组件
    route-permissions:  #路由权限配置(可以配置多个)
      - path: '/route/**'
        permission: 'user:route'
    permission-path-patterns: #权限组件拦截范围 (路由和注解权限共享)
      - /**
    permission-exclude-path-patterns: #权限组件拦截白名单范围 (路由和注解权限共享)
      - /api/xxx

对于一些简单的系统,可能只想基于路由做一些简单的控制,此时可以考虑使用路由组件。

需要注意的是,路由组件支持通过配置文件进行配置

也支持通过实现PermissionService中的loadRoutePermissionConfig方法来进行。自定义加载的方式优先级高于配置文件的方式

路由权限组件同样需要实现PermissionService,动态为当前用户追加权限编码。

实现方式参考第1.5章节。

# 1.7 用户安全锁定组件

在配置文件中新增如下配置参数:

shanhai:
  power:
    lockThreshold: 5 #锁定阈值(默认:5)
    lockThresholdExpire: 3600 #锁定阈值累计有效期(单位:s,默认:1小时)
    lockExpire: 1800 # 锁定时长(单位:s,默认:30分钟)

安全锁定组件相关方法如下:

API说明 API能力 备注
ShanhaiPower.loginLock(Object userFlag) 登录锁定判断 指定用户是否已被锁定,返回Boolean类型
ShanhaiPower.loginLock(Object userFlag,String channel) 登录锁定判断 指定用户&指定登录渠道,是否已被锁定,返回Boolean类型
ShanhaiPower.loginFailure(Object userFlag) 登录失败调用 当用户登录失败时,手动调用触发阈值累计方法
ShanhaiPower.loginFailure(Object userFlag,String channel) 登录失败调用 当用户指定渠道登录失败时,手动调用触发阈值累计方法

# 1.8 统一异常管理

组件异常类 说明
ShanHaiNotLoginException 用户会话鉴权相关异常
ShanHaiNotPermissionException 用户权限相关异常
ShanHaiPowerException 异常基类

可以使用SpringBoot的统一异常管理进行控制

@ExceptionHandler(value = ShanHaiPowerException.class)
public ResponseEntity<?> shanHaiPowerErrorHandler(Exception e) {
    Map<String, Object> resp=new HashMap<>();
    resp.put("code",((ShanHaiPowerException)e).getCode());
    resp.put("message",e.getMessage());
    HttpHeaders headers = new HttpHeaders();
    MediaType mediaType = new MediaType("application","json", StandardCharsets.UTF_8);
    headers.setContentType(mediaType);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).headers(headers).body(resp);
}

# 1.9 扩展-会话持久化

可以通过实现PowerStoreService,来实现自定义的会话持久化。

/**
 * 会话存储服务
 * @author Shmily
 */
public interface PowerStoreService {

    /**
     * 设置缓存失效时间
     * @param key
     * @param time (单位s)
     * @return
     */
    Long expire(String key, int time);

    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    boolean exists(String key);
    /**
     * 查询key过期时间
     * @param key
     * @return
     */
    long ttl(String key);
    /**
     * 删除key
     * @param key
     * @return
     */
    void del(String key);
    /**
     * 读取key对应的值
     * @param key
     * @return
     */
    Object get(String key);
    /**
     * 设置key:value
     * @param key
     * @return
     */
    void set(String key, Object value);
    /**
     * 设置key和过期时间
     * @param key
     * @return
     */
    void set(String key, Object value, long time);

}

# 1.10 扩展-Token生成规则

组件默认集成了uuid和sha512方式的Token生成规则,可以通过配置参数来实现。

同时,也支持自己实现TokenGenerateService,来实现自定义的Token生成规则。

如果需要传入额外参数生成Token,需要在登录的时候使用如下方法登录:

ShanhaiPower.login(Object userFlag,String channel, Map<String, Object> extParams)

# 2.OTP双因素认证扩展(MFA)

山海power集成了HOTP和TOTP,使用时需要引入如下依赖:

<properties>
    <commons-codec.version>1.15</commons-codec.version>
    <zxing-core.version>3.3.3</zxing-core.version>
    <zxing-javase.version>3.3.3</zxing-javase.version>
</properties>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>${commons-codec.version}</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>${zxing-core.version}</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>${zxing-javase.version}</version>
</dependency>

用户和OTP集成方式因人而异,因此需要自行绑定用户身份和OTP密钥并做好存储。

推荐使用TOTP作为双因素认证使用,HOTP的counter容易被爆破,从而降低安全性。

# 2.1 HOTP示例

String qrCodeContent=hotp.getURI(1111,"testuser").toString();
//自动生成密钥
byte[] secret =  SecretGenerator.generate();
//偏移值,可以自行定义
long counter=7891; 
HOTPGenerator hotp = new HOTPGenerator.Builder(secret)
    .withPasswordLength(6)
    .withAlgorithm(HMACAlgorithm.SHA256)
    .build();
//生成OTP协议(可以用于生成二维码)   
String qrCodeContent=hotp.getURI(counter,"testuser").toString();
//校验动态码是否正确
hotp.verify("123456",counter);

# 2.2 TOTP示例

// Generate a secret (or use your own secret)
byte[] secret = SecretGenerator.generate();
TOTPGenerator totp = new TOTPGenerator.Builder(secret)
        .withHOTPGenerator(builder -> {
            builder.withPasswordLength(6);
            builder.withAlgorithm(HMACAlgorithm.SHA256); // SHA256 and SHA512 are also supported
        })
        .withPeriod(Duration.ofSeconds(30))
        .build();
//生成OTP协议(可以用于生成二维码)        
String qrCodeContent=totp.getURI("platName","zhangsan").toString();
//校验动态码是否正确
totp.verify("331197")

# 2.3 生成OTP绑定二维码

int width = 800;// 二维码宽度
int height = 800;// 二维码高度
int margin = 10;// 二维码边距

String logoPath = "d:/xxx.jpg";// LOGO图片路径
int logoSizeMultiple = 3;// 二维码与LOGO的大小比例

String filePath = "d:/otp.jpg";// 指定生成图片文件的保存路径
String fileName = "totp";// 生成的图片文件名
String formatName = "jpg";// 生成的图片格式,可自定义

try {
    // 生成二维码(qrCodeContent即为2.1或2.2中生成的内容)
    BufferedImage qrcode = QRCodeUtils.createQRCode(qrCodeContent, width, height,margin);
    // 添加LOGO
    //           qrcode = QRCodeUtils.createQRCodeWithLogo(qrcode, width, height, logoPath,logoSizeMultiple);
    // 导出到指定路径
    boolean result = QRCodeUtils.generateQRCodeToPath(qrcode, filePath, fileName, formatName);
    System.out.println("执行结果" + result);
} catch (Exception e) {
    e.printStackTrace();
}

可以使用支持OTP协议的客户端进行扫码,如2FAS