Spring Boot整合Shiro实现前后端分离

一、Shiro简介

  Apache Shiro是Java的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。
  Shiro基本功能点如下所示:

图1.1 Shiro基本功能点.png

  Shiro工作流程如下所示:

图1.2 Shiro工作流程-应用程序角度.png

  Shiro内部架构如下所示:

图1.3 Shiro内部架构.png

Shiro官网
认证与Shiro安全框架

二、Spring Boot整合Shiro

  本文实现源码如下,欢迎Star和Fork。

https://github.com/just-right/shiro

2.1 前后端分离

  实现思路:用户登录时生成token信息,设置过期时间,使用Redis存储。前端调用接口时将token作为参数传给服务端,服务端根据token信息认证用户。

参考链接一:一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统
参考链接二:人人开源-renren-fast

  自定义AuthFilter过滤器,继承AuthenticatingFilter重写createToken、isAccessAllowed、onAccessDenied、onLoginFailure方法。
  AuthenticatingFilte类executeLogin方法如下所示:

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    AuthenticationToken token = this.createToken(request, response);
    if (token == null) {
        String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
        throw new IllegalStateException(msg);
    } else {
        try {
            Subject subject = this.getSubject(request, response);
            subject.login(token);
            return this.onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException var5) {
            return this.onLoginFailure(token, var5, request, response);
        }
    }
}
图2-1 前后端分离-Shiro核心类.png

  用户登录时删除旧Token信息,重新生成Token信息,退出登录时删除Token信息。使用Redis存储Token信息时,同时存储已用户ID为键,Token为值和已Token为键、用户ID为值的信息

//用户登录 -- 
String oldToken = tokenService.getUserToken(uid);
/**
 * 删除旧Token信息
 * { token: userId }
 * { userId: [tokenList] }
 */
tokenService.delUserToken(uid);
tokenService.delTokenUser(oldToken);
/**
 * 创建新Token信息
 */
String token = tokenService.createUserToken(uid);
tokenService.createTokenUser(uid,token);
//用户退出登录 -- 
/**
 * 删除Token信息
 */
tokenService.delUserToken(Integer.valueOf(uid));
tokenService.delTokenUser(token);

  整体实现流程图如下所示,图源来自参考链接一,侵删

图2-2 前后端分离-接口请求流程图.png

2.2 多Realm管理

  实现思路:自定义ModularRealmAuthenticator管理多Realm,结合自定义认证Token关联不同的Realm。

参考链接一:SpringBoot Shiro多realm实现免密登录

  SecurityManager和ModularRealmAuthenticator配置如下:

@Bean(value = "securityManager")
public SessionsSecurityManager securityManager(@Qualifier("myRealm") Realm myRealm,@Qualifier("myRealm2") Realm myRealm2) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //指定多 realm  先配置Authenticator-顺序不能相反
    securityManager.setAuthenticator(modularRealmAuthenticator());
    List<Realm> list = Arrays.asList(myRealm,myRealm2);
    securityManager.setRealms(list);
    securityManager.setRememberMeManager(null);
    return securityManager;
}

@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
    //自己重写的ModularRealmAuthenticator
    MyModularRealmAuthenticator modularRealmAuthenticator = new MyModularRealmAuthenticator();
    //至少一个成功
    modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
    return modularRealmAuthenticator;
}

  自定义认证Token,重写getCredentials方法,根据loginType返回不同的比较对象

/**
 * 自定义Token
 * @author: luffy
 */
@Data
@NoArgsConstructor
public class MyAuthToken extends UsernamePasswordToken {
    private String token;
    private String loginType;

    public MyAuthToken(final String username, final String password,final String token,
                     final String loginType) {
        super(username, password);
        this.token = token;
        this.loginType = loginType;
    }

    /**
     * 祖父类  --- AuthenticationToken
     * Object getPrincipal();    --- 资源对象
     * Object getCredentials();  --- 比较对象
     * 1.如果是普通登陆 返回密码
     * 2.如果是Token访问 返回token
     * 目前只有两种Realm
     * ... ...
     * @return
     */
    @Override
    public Object getCredentials() {
        if (IShiroConst.TOKEN_REALM_NAME.equals(this.getLoginType())){
            return getToken();
        }
        return getPassword();
    }
}

  普通登陆Realm认证逻辑如下所示:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    MyAuthToken token = (MyAuthToken) authenticationToken;
    User user =  userService.queryByUserName(token.getUsername());
    if (user == null){
        throw new UnknownAccountException("用户不存在!");
    }
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(token.getPrincipal(), user.getPassword(), getName());
    if (user.getSalt() != null){
        info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
    }
    return info;
}

  Token关联的Realm认证逻辑如下所示:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    MyAuthToken authToken = (MyAuthToken) authenticationToken;
    String token = authToken.getToken();
    /**
     * 存缓存中获取token token为key,uid为value
     */
    if (StringUtils.isEmpty(token) || !TokenUserRedisUtils.isExistedKey(token) || StringUtils.isEmpty(TokenUserRedisUtils.getValueByKey(token))){
        throw new IncorrectCredentialsException("token失效,请重新登录");
    }
    String uid = TokenUserRedisUtils.getValueByKey(token);
    assert uid != null;
    User user =  userService.queryById(Integer.parseInt(uid));
    if (user == null){
        throw new UnknownAccountException("用户不存在!");
    }
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName());
    return info;
}

  自定义ModularRealmAuthenticator,管理多Realm,实现逻辑如下所示:

/**
 * 多Realm配置管理
 * @author: luffy
 */
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        // 强制转换回自定义的CustomizedToken
        MyAuthToken userToken = (MyAuthToken) authenticationToken;
        // 登录类型
        String loginType = userToken.getLoginType();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }
        // 判断是单Realm还是多Realm
        if (typeRealms.size() == 1){
            return doSingleRealmAuthentication(typeRealms.get(0), userToken);
        }
        else{
            return doMultiRealmAuthentication(typeRealms, userToken);
        }
    }
}

三、测试

  用户登录,返回生成的token信息:

图3-1 用户登录.png

  用户携带token信息查询文章(有对应权限):

图3-2 用户携带token信息查询文章.png

  用户携带token信息删除用户(无权限):

图3-3 用户携带token信息删除用户.png

  用户携带token信息退出登录:

图3-4 用户携带token信息退出登录.png

  用户退出登录后携带原token信息删除用户:

图3-5 用户退出登录后携带原token信息删除用户.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351