Shiro结合JWT实现单点登录

简述

Apache Shiro是java的一个安全框架,Shiro可以帮助我们完成认证、授权、加密、会话管理、与Web集成、缓存等。而且Shiro的API也比较简单,这里我们就不进行过多的赘述,想要详细了解Shiro的,推荐看开涛的博客(点这里

在Shiro的强大权限管理的基础上,我们实现单点登录就容易了很多,结合我上篇博客所讲的JSON Web Token(推荐先看这篇博客)就可以完成单点登录系统。

实现过程

在使用Shiro实现登录的时候,将登录成功的信息包括Token信息返回给前端,前端在请求后台时,将Token信息存入请求头中。配置自定义拦截器,拦截所有URL请求,取出请求头信息中的Token信息,对Token信息进行验证,对于redis中存在的登录时生成的Token信息,如果Token信息正确,则确认该用户已经登录,否则拒绝请求,返回401错误。

1.引入所需jar包

        <!--json-web-token-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.登录认证

要实现单点登录功能,首先要完成的就是登录功能,这里我们使用Shiro的认证来完成登录。

2.1 spring-shiro的配置文件
<!--配置SecurityManager-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realms">
            <list>
                <ref bean="shiroRealm"/>
            </list>
        </property>
    </bean>

    <!--配置Realm-->
    <!--直接配置实现了org.apache.shiro.realm.Realm接口的bean-->
    <bean id="shiroRealm" class="com.why.authority.realms.ShiroRealm">
        <!-- 凭证匹配器:配置登录验证所使用的加密算法-->
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="sha-512"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <!-- shiro拦截器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/user/index"/>
        <!--配置哪些页面需要受保护
            1. anon 可以被匿名访问
            2. authc 必须认证(登录)后才可能访问的页面
            3. logout 登出
        -->

        <!--自定义filters,该拦截器即为实现单点登录的拦截器-->
        <property name="filters">
            <map>
                <entry key="acf">
                    <bean class="com.why.authority.filter.AccessingControlFilter"/>
                </entry>
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /user/index=anon
                /user/login=anon
                /user/content= acf
                /** = acf
            </value>
        </property>
    </bean>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
2.2 登录方法

Controller:

@RequestMapping(value = {"/login"}, method = RequestMethod.POST)
    @ResponseBody
    public WhyResult content(@RequestParam("usercode") String usercode, @RequestParam("password") String password) {

        String userInfoKey = "aum:user:" + usercode;
        String tokenKey = "aum:token:"+usercode;

        try {
            if(StringUtils.isBlank(usercode) || StringUtils.isBlank(password)){
                throw new UnknownAccountException();
            }
            //1. 执行登录
            //把用户名和密码封装为UsernamePasswordToken对象
            UsernamePasswordToken token = new UsernamePasswordToken(usercode, password);
            SecurityUtils.getSubject().login(token);

            //2.获取用户信息userEntity,redis中不存在则存入redis
            UserEntity userEntity = new UserEntity();
            //2.1 从redis中获取或从数据库中获取
            String strUserInfo = JedisCacheUtil.get(userInfoKey);
            if (!StringUtils.isBlank(strUserInfo)) {
                userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
            } else {
                userEntity = addUserInfoToRedis(usercode, userInfoKey);
            }
            //3.生成Token信息并保存到redis
            LoginEntity loginEntity = addTokenToRedis(userEntity,tokenKey);
            return WhyResult.build(200,"登录成功!",loginEntity);
            //所有认证异常的父类
        } catch (AuthenticationException e) {
            logger.error("登录失败!",e);
            return WhyResult.build(401,"用户名或密码错误!");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

自定义Realm

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //1.把AuthenticationToken转换为UsernamePasswordToken
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;

        //2.从UsernamePasswordToken中获取userCode
        String userCode = usernamePasswordToken.getUsername();
        String userInfoKey = "aum:user:" + userCode;
        UserEntity userEntity;
        //3.获取用户信息userEntity
        //3.1 从redis中获取
        String strUserInfo;
        try {
            strUserInfo = JedisCacheUtil.get(userInfoKey);
            if (!StringUtils.isBlank(strUserInfo)) {
                userEntity = JacksonJsonUntil.jsonToPojo(strUserInfo, UserEntity.class);
            } else {
                userEntity = addUserAndGetUser(userCode, userInfoKey);
            }
        } catch (Exception e) {
            userEntity = addUserAndGetUser(userCode, userInfoKey);
        }
        //6.根据用户的情况,来构建AuthenticationInfo对象并返回
        String credentials = userEntity.getPassword();
        //使用ByteSource.Util.bytes()来计算盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(userCode);

        return new SimpleAuthenticationInfo(userEntity, credentials, credentialsSalt, getName());

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

3. 自定义拦截器

该拦截器是在spring-shiro.xml文件中配置的自定义拦截器,原理就是拦截每个请求,验证URL请求头信息中的Token信息是否过期,是否被篡改。

@Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //是否验证通过
        boolean bool = false;
        try {
            HttpServletRequest req = WebUtils.toHttp(servletRequest);
            String firstLoginToken = req.getParameter("token");

            //从token中获得信息
            Claims claims = TokenUtil.getClaims(firstLoginToken);
            String userCode = claims.getSubject();
            String userId = claims.getId();

            String redisLoginKey = "aum:token:" + userCode;
            String redisToken = JedisCacheUtil.get(redisLoginKey);
            if(!StringUtils.isBlank(redisToken)){
                String[] arrayRedisToken = redisToken.split("@");
                //将用户传过来的token和redis中的做对比,若一样,认为已经登录
                if (arrayRedisToken[0].equals(firstLoginToken)) {
                    //比较这次访问与登录的时间间隔有多少分钟,如果大于5分钟,则更新redis中的上次访问时间信息,将过期时间从新设定为30分钟
                    long diffMin = TokenUtil.CompareTime(arrayRedisToken[1]);
                    if (diffMin >= 5) {
                        String currentAccessTime = PasswordUtil.base64Encoede(String.valueOf(System.currentTimeMillis()));
                        //更新redis中的token登录信息
                        JedisCacheUtil.set(redisLoginKey, arrayRedisToken[0] + "@" + currentAccessTime, 30 * 60);
                    }
                    bool=true;
                }
            }
        } catch (Exception e) {
            return bool;
        }
        return bool;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

至此为止,关键代码已经展示完了,现在实现的仅仅是最基础的单点登录,还需要进行更多的安全检查和验证,这里就不介绍了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值