1. 介绍

提供开箱即用的spring security安全防护及配置,initilizer生成的工程包含基本的使用及示例。

2. 使用

gradle中

implementation('org.yunchen.gb:gb-plugin-springsecurity:1.4.0.0.M1')

3. 描述

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

基本配置

#spring security
security.basic.enabled: false
gb:
    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

4. 内置安全domain类

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

name 描述

BaseRole

角色

BaseUser

用户

BaseUserBaseRole

用户角色映射

Requestmap

访问控制

SystemLoginRecord

登录日志

5. 开发规约

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

5.1. 初始数据

在系统的startup类的init方法中,默认有幂等的几个初始数据的方法。

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

5.2. 登录事件

发生系统登录事件时,会自动调用在startup类的onAuthenticationSuccess方法或onAuthenticationFailure方法,从而实现登录日志记录.

示例如下:

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        //保存入登录日志
        Map map=[:];
        String username=authentication.getPrincipal().username;
        map.remoteaddr=request.getRemoteAddr();
        map.sessionId=request.getSession().getId();
        map.loginTime=new Date();
        Timer timer=new Timer();
        //100毫秒后分离线程执行
        timer.runAfter(100){
            SystemLoginRecord.withNewSession{
                SystemLoginRecord systemLoginRecord=new SystemLoginRecord(map);
                systemLoginRecord.baseUser=BaseUser.findByUsername(username);
                systemLoginRecord.save(flush:true);
            }
        }
    }

5.3. 获取当前登录用户

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

BaseUser currentUser=BaseUser.read(gbSpringSecurityService.principal.id);

5.4. 当前用户鉴权操作

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

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

5.4.1. controller中

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

println sessionRegistry.allPrincipals*.username;

详细的演示在WorkspaceController.groovy和LogoutController中。

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

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

5.4.2. 页面中

参看themyleaf3页面的示例

6. 提供辅助类

提供辅助类:

GbSpringSecurityUtils

    ifAllGranted(String roles)    
    ifNotGranted(String roles)   
    ifAnyGranted(String roles)   
    isAjax(HttpServletRequest request)   ajax
    reauthenticate(String username, String password)  
    PasswordEncoder findPasswordEncoder(String algorithm)  //获取指定算法的PasswordEncoder
GbSpringSecurityService
使@Autowired 
    getPrincipal()        principal  anonymous
     org.yunchen.gb.plugin.springsecurity.userdetails.CoreUser 
    getCurrentUser()    BaseUser
    encodePassword(String password)
    encodePassword(String password, Object salt = null)
    isLoggedIn()
    clearCachedRequestmaps()   访
    PasswordEncoder findPasswordEncoder(String algorithm)  //获取指定算法的PasswordEncoder
thmeleaf taglib
    1. <xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    2. <span  sec:authentication="principal.username" />
    3.  <div  sec:authorize="hasAnyRole('ROLE_ADMIN')">

7. 启用cors的处理

若其他域名的应用系统使用本系统的rest接口,出现跨域无法访问的403 错误时,按如下操作:

修改application.yml中的 cors值为 enable

gb:
    springsecurity:
      active: true
      frameOptions: sameOrigin   #disabled,deny,sameOrigin
      csrf: disable
      cors: enable
      corsConfig:
        allowCredentials: true # true or false
        allowedOrigins:  '*'  # * or http://localhost:8080,http://somesite.com.cn
        allowedHeaders:  '*'  #
        allowedMethods:  '*' # GET,POST or *
        corsPath: /**

8. jsr250 与 requestmap混合工作

8.1. 配置SecurityConfig类

在config目录创建ProjectSecurityConfig类

import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter

@Configuration
@EnableAutoConfiguration
//@EnableGlobalMethodSecurity(securedEnabled=true)
@EnableGlobalMethodSecurity(jsr250Enabled=true)
class ProjectSecurityConfig extends WebSecurityConfigurerAdapter{
}
也可启用securedEnabled ,就可在项目中使用@Secured注解
jsr250Enabled 可以使用@PermitAll,@RolesAllowed,@DenyAll 三个注解

8.2. 在controller类中使用

    //@Secured("ROLE_ADMIN")
    //@PermitAll
    //@RolesAllowed("ROLE_USER,ROLE_ADMIN")
    @DenyAll
    public Map index2(){
        return [result:true]
    }
目前方法上的@PermitAll注解无法工作, 需要修改在requestMap表中最后一行, /** 配置为 permitAll

9. 国密算法支持

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

9.1. 修改application.yml文件

gb:
    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类添加的,后面是加密后的字符

10. 配置自定义算法

在项目中存在使用用户id作为盐值的情况,要支持此种情况需要如下配置

10.1. 配置applicaiton.yml

gb:
  springsecurity:
    password:
      encodeHashAsBase64: false
      algorithm: custom # bcrypt,pbkdf2,SHA-512,SHA-384,SHA-256,SHA-224,SHA-1,MD5,MD2,SM3,SM4,custom   (1)
      useCustomMethodAlgorithm: bcrypt    (2)
1 配置此项为custom ,在系统中使用CustomPasswordEncoder
2 配置一个辅助的算法encoder,可为上一项除去custom外的任意值

10.2. 添加encoder回调方法

在任何标有@GbBootstrap注解的类中,如Startup类,添加如下方法

    public String customPasswordEncoder(CoreUser currentLoadUser, PasswordEncoder passwordEncoder,CharSequence plainTextPassword){
        //passwordEncoder 是useCustomMethodAlgorithm配置项制定的算法加密器
        String encodeStr=passwordEncoder.encode(plainTextPassword)
        return encodeStr;
        //因spring security5后不再使用salt,可以自己定制盐值加密类,以便与遗留系统集成
        //return Md5Encoder.encode(plainTextPassword,currentLoadUser.id);
    }

10.3. 添加matches回调方法

在任何标有@GbBootstrap注解的类中,如Startup类,添加如下方法

    public boolean customPasswordMatches(CoreUser currentLoadUser, PasswordEncoder passwordEncoder,CharSequence rawPassword, String encodedPassword){
        //passwordEncoder 是useCustomMethodAlgorithm配置项制定的算法加密器
        return passwordEncoder.matches(rawPassword,encodedPassword)
        //因spring security5后不再使用salt,可以自己定制盐值加密类,以便与遗留系统集成
        //return Md5Encoder.encode(rawPassword,currentLoadUser.id).equals(encodedPassword);
    }

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

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

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

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

10.5. 修改系统的密码加密

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

class BaseUser implements Serializable {
    。。。。。。
    protected void encodePassword() {
        CoreUser coreUser=new CoreUser(username, password, enabled, !accountExpired, !passwordExpired, !accountLocked, [], id)
        password = GbSpringUtils.getBean("passwordEncoder").encode(coreUser,password)
    }
}

11. 配置验证时自定义加载用户

在实际项目中有支持自定义字段匹配用户名的需求,如手机号,邮箱等。

11.1. 配置applicaiton.yml

gb:
  springsecurity:
        password:
          useCustomLoadUser: true

11.2. 添加loaduser回调方法

在任何标有@GbBootstrap注解的类中,如Startup类,添加如下方法

    public BaseUser customLoadUser(String inputUsername){
    //示例,可自由定制,返回BaseUser实例对象即可
        return BaseUser.findByUsernameOrEmailOrPhone(inputUsername,inputUsername,inputUsername);
    }

11.3. 安全事件

安全事件AppSecurityAuthSuccessEvent、AppSecurityAuthFailureEvent和安全事件基类AppSecurityEvent; 获取AppSecurityAuthSuccessEvent事件的source是一个Map,内容是:[request:request,response:response,authentication:authentication] 获取AppSecurityAuthFailureEvent事件的source是一个Map,内容是:[request:request,response:response,authenticationException:authenticationException]

若订阅安全基类AppSecurityEvent事件,则能收到全部框架发布的安全事件。 authenticationException 是AccountExpiredException、CredentialsExpiredException、 DisabledException、 LockedException、 SessionAuthenticationException、 CaptchaVerificationFailedException六类异常中的一个。

订阅示例:

@Configuration
@Slf4j
class NewSecurityAuthSuccessAppListener implements ApplicationListener<AppSecurityAuthSuccessEvent> {
    @Override
    void onApplicationEvent(AppSecurityAuthSuccessEvent event) {
        println "login user is : ${event.source.authentication.principal.username}";
    }
}

12. 横向扩展

提供session复制,jwt存储认证的横向扩展能力;支持cookie存储认证信息的横向扩展能力。

12.1. session复制

使用redis进行session复制

12.2. JWT方案

12.3. cookie存储

考虑到使用传统mvc方案的用户,在升级到jwt时,需要前端使用MVVM框架或VUE,成本较高。 因此针对传统mvc模式,提供此cookie存储方案,快速解决横向扩展,减少代码复杂度和迁移成本。

需要客户端浏览器开启cookie支持

12.3.1. 使用步骤

在application.yml中增加配置

gb:
   springsecurity:
    scale:
      enableCookie: true #使用cookie存储认证信息
      httpOnly: true #不允许客户端js读取
      secure: false #只支持https协议
      tokenValiditySeconds: 1209600 # 14 days
      domainName:         #cookie域名,为空或不设置则使用访问路径localhost或IP

经过以上两步后,系统用户登录后,会自动将认证信息写入浏览器cookie; 无论是服务端重启或多服务轮询,都会优先检验cookie,再检验sessionId.

用户在系统中,主动登出/logout/index 或访问/logoff 时 或关闭浏览器时,系统都会自动清除cookie.

12.4. 使用redis存储requestmap

12.4.1. 添加项目的data-radis插件

implementation('org.yunchen.gb:gb-plugin-data-redis:1.4.0.0.M1')

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

gb.springsecurity.requestmap.gatherToRedis: true;

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

12.4.4. 默认增加的redis项

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

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