介绍

提供开箱即用的spring security安全防护及配置。

版本历史

  • 0.2.1

使用

使用gradle 插件

添加插件

gradle中

    implementation('io.github.baseinsight:plugin-springsecurity-starter:x.x.x')

代码

因为JPA需要的entity、repository类等需要根据项目来具体定制,因此可下载示例基础代码来了解。

描述

是spring security官网的除了Annotation注解、yml集中配置外的第三种标准方式,将授权、鉴权都存储入数据库的模式。spring security参考文档

内置安全entity类

增加entity类5个,增加controller类及相关页面

name 描述

BaseRole

角色

BaseUser

用户

BaseUserBaseRole

用户角色映射

Requestmap

访问控制

SystemLoginRecord

登录日志

编写核心安全回调类

因为项目的安全entity类是根据项目变化的,因此需要在工程中创建CoreSecurityComponent类,并实现CoreSecurityOperater接口的所有方法。

// springsecurity的组件实现类,将安全的的查询类库与本地应用的表格结合
@Component
public class CoreSecurityComponent implements CoreSecurityOperater {
    @Autowired
    RequestmapRepository requestmapRepository;
    @Autowired
    BaseUserRepository baseUserRepository;
    @Autowired
    BaseUserBaseRoleRepository baseUserBaseRoleRepository;
    @Autowired
    BaseRolePrivilegeRepository baseRolePrivilegeRepository;
    @Override
    public Object findUserByUsername(String username) {
        return baseUserRepository.findByUsername(username);
    }

    @Override
    public Object findUserById(Object id) {
        return baseUserRepository.findById((String)id);
    }

    @Override
    public List findAllUserAuthorities(Object user) {
        List<String> roleNameList= new ArrayList<String>();
        List<BaseUserBaseRole> list=baseUserBaseRoleRepository.findAllByBaseUserId(((BaseUser)user).getId());
        list.forEach(baseUserBaseRole->{
            roleNameList.add(baseUserBaseRole.getBaseRole().getAuthority());
        });
        return roleNameList;
    }

    @Override
    public List findAllUserPrivileges(List<String> roleNames) {
        List<String> privilegeNameList= new ArrayList<String>();
        List<BaseRolePrivilege> list=baseRolePrivilegeRepository.findAllByBaseRoleIdIn(roleNames);
        list.forEach(baseRolePrivilege->{
            privilegeNameList.add(baseRolePrivilege.getPrivilege().getName());
        });
        return privilegeNameList;
    }

    @Override
    public List findAllRequestmaps() {
        return requestmapRepository.findAll(Sort.by(Sort.Direction.ASC,"id"));
    }

    @Override
    public List findAllRequestmapsByRoleName(String roleName) {
        return requestmapRepository.findAllByConfigAttributeLike(roleName);
    }
}

开发规约

使用系统封装的SpringSecurityUtils类或SpringSecurityService类获取登录用户信息。 因为用户entity类中的外键懒加载原因,不建议将用户entity实例存储进session中.

初始数据

在系统的ApplicationRunner或CommandLineRunner类的run方法中,默认有幂等的几个初始数据的方法。

createDefaultRoles(); //初始化系统角色
createDefaultUsers();//初始化系统用户
createRequestMap();//初始化系统访问控制列表
initMenu();//初始化系统菜单

登录事件

发生系统登录事件时,会自动调用AuthenticationEvents类的相关方法,从而实现登录日志记录.

示例如下:

@Component
public class AuthenticationEvents {
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent success) {
        // ...
        Object source=success.getSource();
        if(source instanceof UsernamePasswordAuthenticationToken){
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=(UsernamePasswordAuthenticationToken)source;
            User user=(User) usernamePasswordAuthenticationToken.getPrincipal();
            user.getUsername();
            WebAuthenticationDetails webAuthenticationDetails= (WebAuthenticationDetails)usernamePasswordAuthenticationToken.getDetails();
            if(webAuthenticationDetails!=null){
                webAuthenticationDetails.getRemoteAddress();
                webAuthenticationDetails.getSessionId();
            }
        }
        if(source instanceof AccessToken){
            AccessToken accessToken=(AccessToken)source;
            User user=(User) accessToken.getPrincipal();
            user.getUsername();
        }
        System.out.println(success.toString());
    }

    @EventListener
    public void onFailure(AbstractAuthenticationFailureEvent failures) {
        Object source=failures.getSource();
        if(source instanceof UsernamePasswordAuthenticationToken){
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=(UsernamePasswordAuthenticationToken) source;
        }
        if(source instanceof AccessToken){
            AccessToken accessToken=(AccessToken)source;
        }
        Exception exception=failures.getException();
        System.out.println(failures.toString());
    }
}

获取当前登录用户

使用注入的springSecurityService获取当前登录用户:

BaseUser currentUser=baseUserRepository.findById(gbSpringSecurityService.principal.id);

当前用户鉴权操作

使用SpringSecurityUtils类进行用户权限鉴别.

println SpringSecurityUtils.getPrincipalAuthorities();
println SpringSecurityUtils.ifAnyGranted("ROLE_USER,ROLE_ADMIN");
println SpringSecurityUtils.ifAllGranted("ROLE_USER,ROLE_ADMIN");
println SpringSecurityUtils.ifNotGranted("ROLE_USER,ROLE_ADMIN");

controller中

使用注入的sessionRegistry获取当前登录系统的用户数目。

println sessionRegistry.allPrincipals*.username;

同时在线用户数目,有application.yml中的sessionAuthenticationStrategy部分的配置决定.

base:
    springsecurity:
      sessionAuthenticationStrategy:
        maximumSessions: 1  #//-1 为不限,1为只可登录一个用户实例   不可为0
        maxSessionsPreventsLogin: false  #// true 为后登陆用户异常,false 为先登陆用户session过期
        expiredUrl: /login/concurrentSession  #为先登陆用户session过期,引导至此路径

提供辅助类

提供辅助类:

SpringSecurityUtils类
静态方法
    ifAllGranted(String roles)    当前用户是否全部授予角色
    ifNotGranted(String roles)   当前用户是否全部未授予角色
    ifAnyGranted(String roles)   当前用户是否授予其中任一角色
    isAjax(HttpServletRequest request)   当前是否ajax请求
    reauthenticate(String username, String password)  重新认证
    PasswordEncoder findPasswordEncoder(String algorithm)  //获取指定算法的PasswordEncoder
SpringSecurityService类
需要使用@Autowired 注入
    getPrincipal()        获取当前登录principal ,匿名用户为字符串 anonymous
    注意:登录用户为 CoreUser 的实例
    getCurrentUser()   获取当前用户实例 (BaseUser)
    encodePassword(String password)
    encodePassword(String password, Object salt = null)
    isLoggedIn()
    clearCachedRequestmaps()   清除当前缓存的访问控制列表
    PasswordEncoder findPasswordEncoder(String algorithm)  //获取指定算法的PasswordEncoder

启用cors的处理

默认系统已启用cors

修改application.yml中的 cors值为 enable或disable

base:
    springsecurity:
      cors: disable

国密算法支持

增加国密算法SM3,SM4的支持

修改application.yml文件

base:
    springsecurity:
      password:
        encodeHashAsBase64: false
        algorithm: SM3 # bcrypt,pbkdf2,SHA-512,SHA-384,SHA-256,SHA-224,SHA-1,MD5,MD2,SM3,SM4
        sm4Key: 86C63180C2806ED1F47B859EE501215C
sm4Key也可不设置,则会默认使用内置的32位16进制密钥。

加密后的效果

admin:{SM3}dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6
user:{SM3}92e7fbdcca8b9f36be0638e48e77cbeeb49ef15886b6cd12d46e09d74a232a81

TIP:其中的{idForEncode} 是springsecurity的DelegatingPasswordEncoder类添加的,后面是加密后的字符

配置去掉加密后的算法标识

spring security5后,加密的字符串前面会自动添加算法标识{math},如{bcrypt}$2a$10$e8zurQgiO8s5O6rYwMUF..XapBU1WqWi8fmZ895z4lnW8QliEDWYW

可以在application.yml中添加如下配置,去除算法标识,以便与遗留系统集成

base:
  springsecurity:
    password:
        withoutIdPrefix: true
携带算法标识是一个很好的习惯,不推荐将其摘除。可以采用中间视图的形式绕开标识问题与遗留系统集成。

修改系统的密码加密

系统中的用户密码加密在BaseUser这个entity类中

    @jakarta.persistence.PrePersist
    public void prePersist() {
        encodePassword();
    }

    @jakarta.persistence.PreUpdate
    public void preUpdate() {
        if(!password.equals(passwordTransient)){
            encodePassword();
        }
    }
       protected void encodePassword() {
        password = ((PasswordEncoder)SpringUtils.getBean("passwordEncoder")).encode(password);
    }

可修改配置

默认系统已进行的基本安全配置,若希望修改,可参照如下在yml文件中逐一变更

#spring security
security.basic.enabled: false
base:
    springsecurity:
      csrf: disable
      cors: disable
      frameOptions: disabled   #disabled,deny,sameOrigin
      csrf: disable
      cors: enable
      corsConfig:
        allowCredentials: true # true or false
        allowedOrigins:  '*'  # * or http://localhost:8080
        allowedHeaders:  '*'  #
        allowedMethods:  '*' # GET,POST or *
        corsPath: /**
      headers:
        - {Access-Control-Expose-Headers: WWW-Authenticate,Authorization,Set-Cookie,X-Frame-Options}
        - {Access-Control-Max-Age: 3600}
      ajaxHeader: X-Requested-With
      password:
        encodeHashAsBase64: false
        algorithm: bcrypt # bcrypt,pbkdf2,SHA-512,SHA-384,SHA-256,SHA-224,SHA-1,MD5,MD2
      securityConfigType :  Requestmap
      userLookup:
        userDomainClassName: org.yunchen.gb.example.demo.domain.core.BaseUser
        authorityJoinClassName: org.yunchen.gb.example.demo.domain.core.BaseUserBaseRole
      authority.className: org.yunchen.gb.example.demo.domain.core.BaseRole
      requestMap.className: org.yunchen.gb.example.demo.domain.core.Requestmap
      apf:     #/** authenticationProcessingFilter */
        filterProcessesUrl: /login/authenticate
      auth:
        loginFormUrl: /login/auth
        alreadyLogin: /login/alreadyLogin #注释此行,则不再做当前session是否登录检查
        useForward: false
      adh:     #/*accessDeniedHandler*/
        errorPage: /login/denied
        ajaxErrorPage: /login/ajaxDenied
        useForward: true
      failureHandler:
        defaultFailureUrl: /login/authfail
        defaultAjaxFailureUrl: /login/authajaxfail
      successHandler:
        defaultTargetUrl: /workspace/index  #登录成功后,若没有rediretUrl则引导进此url
        ajaxSuccessUrl: /login/ajaxSuccess
        #如注释systemloginRecord 则不进行登录日志记录
        systemloginRecord: org.yunchen.gb.example.demo.domain.core.SystemLoginRecord
      logout:
        afterLogoutUrl: /
        filterProcessesUrl: /logoff
      sessionAuthenticationStrategy:
        maximumSessions: 1  #//-1 为不限,1为只可登录一个用户实例   不可为0
        maxSessionsPreventsLogin: false  #// true 为后登陆用户异常,false 为先登陆用户被踢出
        expiredUrl: /login/concurrentSession

使用redis存储requestmap

添加项目的radis插件

    implementation('io.github.baseinsight:plugin-redis-starter:x.x.x')

增加yml文件配置,启用此功能

base.springsecurity.requestmapGatherToRedis: true;

每次用户访问,系统会自动从redis server下载requestmap的服务器配置

默认增加的redis项

  1. 键值:base:spring:security:compiledJson

  2. 键值:base:spring:security:compiledJsonMd5