SpringBoot+Shiro+Jwt实现登录认证

详细请查看https://zhuanlan.zhihu.com/p/391839846

1. 概述

1.1 SpringBoot

这个就没什么好说的了,能看到这个教程的,估计都是可以说精通了SpringBoot的使用

1.2 Shiro

一个安全框架,但不只是一个安全框架。它能实现多种多样的功能。并不只是局限在web层。在国内的市场份额占比高于SpringSecurity,是使用最多的安全框架

可以实现用户的认证和授权。比SpringSecurity要简单的多。

1.3 Jwt

我的理解就是可以进行客户端与服务端之间验证的一种技术,取代了之前使用Session来验证的不安全性

为什么不适用Session?

原理是,登录之后客户端和服务端各自保存一个相应的SessionId,每次客户端发起请求的时候就得携带这个SessionId来进行比对

  1. Session在用户请求量大的时候服务器开销太大了
  2. Session不利于搭建服务器的集群(也就是必须访问原本的那个服务器才能获取对应的SessionId)

它使用的是一种令牌技术

Jwt字符串分为三部分

  1. Header

    存储两个变量

    1. 秘钥(可以用来比对)
    2. 算法(也就是下面将Header和payload加密成Signature)
  2. payload

    存储很多东西,基础信息有如下几个

    1. 签发人,也就是这个“令牌”归属于哪个用户。一般是userId
    2. 创建时间,也就是这个令牌是什么时候创建的
    3. 失效时间,也就是这个令牌什么时候失效
    4. 唯一标识,一般可以使用算法生成一个唯一标识
  3. Signature

    这个是上面两个经过Header中的算法加密生成的,用于比对信息,防止篡改Header和payload

然后将这三个部分的信息经过加密生成一个JwtToken的字符串,发送给客户端,客户端保存在本地。当客户端发起请求的时候携带这个到服务端(可以是在cookie,可以是在header,可以是在localStorage中),在服务端进行验证

好了,废话不多说了,下面开始实战,实战分为以下几个部分

  1. SpringBoot整合Shiro
  2. SpringBoot整合Jwt
  3. SpringBoot+Shiro+Jwt
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2. SpringBoot整合Shiro

两种方式:

  1. 将ssm的整合的配置使用java代码方式在springBoot中写一遍
  2. 使用官方提供的start

2.1 使用start整合springBoot

pom.xml

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.0</version>
</dependency>
<!--注意不要写成shiro-spring-boot-starter-->

application.properties

shiro.loginUrl="xxx"
#认证不通过的页面
shiro.UnauthorizedUrl="xxx"
#授权不通过的跳转页面

创建ShiroConfig.java进行一些简单的配置

@Configuration
public class SpringShiroConfig {
    @Bean
    public Realm customRealm() {
        return new CustomRealm();
    }
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        // 关闭 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        // 哪些请求可以匿名访问
        chain.addPathDefinition("/login", "anon");      // 登录接口
        chain.addPathDefinition("/notLogin", "anon");   // 未登录错误提示接口
        chain.addPathDefinition("/403", "anon");    // 权限不足错误提示接口
        // 除了以上的请求外,其它请求都需要登录
        chain.addPathDefinition("/**", "authc");
        return chain;
    }
    // Shiro 和 Spring AOP 整合时的特殊设置
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

//还有关闭ShiroDao功能

创建自定义的Realm

public class CustomRealm extends AuthorizingRealm {
    private static final Set<String> tomRoleNameSet = new HashSet<>();
    private static final Set<String> tomPermissionNameSet = new HashSet<>();
    private static final Set<String> jerryRoleNameSet = new HashSet<>();
    private static final Set<String> jerryPermissionNameSet = new HashSet<>();
    static {
        tomRoleNameSet.add("admin");
        jerryRoleNameSet.add("user");
        tomPermissionNameSet.add("user:insert");
        tomPermissionNameSet.add("user:update");
        tomPermissionNameSet.add("user:delete");
        tomPermissionNameSet.add("user:query");
        jerryPermissionNameSet.add("user:query");
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info =  new SimpleAuthorizationInfo();
        if (username.equals("tom")) {
            info.addRoles(tomRoleNameSet);
            info.addStringPermissions(tomPermissionNameSet);
        } else if (username.equals("jerry")) {
            info.addRoles(jerryRoleNameSet);
            info.addStringPermissions(jerryPermissionNameSet);
        }
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        if (username == null)
            throw new UnknownAccountException("用户名不能为空");
        SimpleAuthenticationInfo info = null;
        if (username.equals("tom"))
            return new SimpleAuthenticationInfo("tom", "123", CustomRealm.class.getName());
        else if (username.equals("jerry"))
            return new SimpleAuthenticationInfo("jerry", "123", CustomRealm.class.getName());
        else
            return null;
    }
}

2.2 不使用starter

<!-- 自动依赖导入 shiro-core 和 shiro-web -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
</dependency>

编写 Shiro 的配置类:ShiroConfig

将 Shiro 的配置信息(spring-shiro.xml 和 spring-web.xml)以 Java 代码配置的形式改写:

@Configuration
public class ShiroConfig {
    @Bean
    public Realm realm() {
        return new CustomRealm();
    }
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shirFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        shiroFilterFactoryBean.setLoginUrl("/loginPage");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/loginPage", "anon");
        filterChainDefinitionMap.put("/403", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/hello", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }
    /* ################################################################# */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制指定注解的底层实现使用 cglib 方案
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

编写 Controller

与 Shiro 和 SSM 的整合一样。略

编写 Thymeleaf 页面

3. SpringBoot整合Jwt

3.1 依赖

1. springboot
2. java-jwt--核心依赖
3. jjwt--java版本的辅助帮助模块

3.2 代码

  1. 创建JwtUtil

    package cn.coderymy.utils;
    
    import java.util.*;
    import com.auth0.jwt.*;
    import com.auth0.jwt.algorithms.Algorithm;
    import io.jsonwebtoken.*;
    import org.apache.commons.codec.binary.Base64;
    
    import java.util.*;
    
    
    public class JwtUtil {
    
        // 生成签名是所使用的秘钥
        private final String base64EncodedSecretKey;
    
        // 生成签名的时候所使用的加密算法
        private final SignatureAlgorithm signatureAlgorithm;
    
        public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
            this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
            this.signatureAlgorithm = signatureAlgorithm;
        }
    
        /**
         * 生成 JWT Token 字符串
         *
         * @param iss       签发人名称
         * @param ttlMillis jwt 过期时间
         * @param claims    额外添加到荷部分的信息。
         *                  例如可以添加用户名、用户ID、用户(加密前的)密码等信息
         */
        public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
            if (claims == null) {
                claims = new HashMap<>();
            }
    
            // 签发时间(iat):荷载部分的标准字段之一
            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);
    
            // 下面就是在为payload添加各种标准声明和私有声明了
            JwtBuilder builder = Jwts.builder()
                    // 荷载部分的非标准字段/附加字段,一般写在标准的字段之前。
                    .setClaims(claims)
                    // JWT ID(jti):荷载部分的标准字段之一,JWT 的唯一性标识,虽不强求,但尽量确保其唯一性。
                    .setId(UUID.randomUUID().toString())
                    // 签发时间(iat):荷载部分的标准字段之一,代表这个 JWT 的生成时间。
                    .setIssuedAt(now)
                    // 签发人(iss):荷载部分的标准字段之一,代表这个 JWT 的所有者。通常是 username、userid 这样具有用户代表性的内容。
                    .setSubject(iss)
                    // 设置生成签名的算法和秘钥
                    .signWith(signatureAlgorithm, base64EncodedSecretKey);
    
            if (ttlMillis >= 0) {
                long expMillis = nowMillis + ttlMillis;
                Date exp = new Date(expMillis);
                // 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
                builder.setExpiration(exp);
            }
    
            return builder.compact();
        }
    
    
        /**
         * JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
         * 而 头部 和 荷载部分是由 Base64 编码算法生成,是可以反向反编码回原样的。
         * 这也是为什么不要在 JWT Token 中放敏感数据的原因。
         *
         * @param jwtToken 加密后的token
         * @return claims 返回荷载部分的键值对
         */
        public Claims decode(String jwtToken) {
    
            // 得到 DefaultJwtParser
            return Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(base64EncodedSecretKey)
                    // 设置需要解析的 jwt
                    .parseClaimsJws(jwtToken)
                    .getBody();
        }
    
    
        /**
         * 校验 token
         * 在这里可以使用官方的校验,或,
         * 自定义校验规则,例如在 token 中携带密码,进行加密处理后和数据库中的加密密码比较。
         *
         * @param jwtToken 被校验的 jwt Token
         */
        public boolean isVerify(String jwtToken) {
            Algorithm algorithm = null;
    
            switch (signatureAlgorithm) {
                case HS256:
                    algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                    break;
                default:
                    throw new RuntimeException("不支持该算法");
            }
    
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(jwtToken);  // 校验不通过会抛出异常
    
    
            /*
                // 得到DefaultJwtParser
                Claims claims = decode(jwtToken);
    
                if (claims.get("password").equals(user.get("password"))) {
                    return true;
                }
            */
    
            return true;
        }
    
        public static void main(String[] args) {
            JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
    
            Map<String, Object> map = new HashMap<>();
            map.put("username", "tom");
            map.put("password", "123456");
            map.put("age", 20);
    
            String jwtToken = util.encode("tom", 30000, map);
    
            System.out.println(jwtToken);
            /*
            util.isVerify(jwtToken);
            System.out.println("合法");
            */
    
            util.decode(jwtToken).entrySet().forEach((entry) -> {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            });
        }
    }
    

    <font color="yellow">解析:</font>

    1. <font color="red">在创建JwtUtil对象的时候需要传入几个数值</font>
      1. 这个用户,用来生成秘钥
      2. 这个加密算法,用来加密生成jwt
    2. 通过jwt数据获取用户信息的方法(decode())
    3. 判断jwt是否存在或者过期的方法
    4. 最后是测试方法
  2. 创建一个Controller

    1. 登录的Controller
      1. 获取username和password,进行与数据库的校验,校验成功执行下一步,失败直接返回
      2. 使用创建JwtUtil对象,传入username和需要使用的加密算法
      3. 创建需要加在载荷中的一些基本信息的一个map对象
      4. 创建jwt数据,传入username,保存时间,以及基本信息的map对象
    2. 校验Controller
      1. 获取前台传入的Jwt数据
      2. 使用JWTUtil中的isVerify进行该jwt数据有效的校验

4. SpringBoot+Shiro+Jwt

  1. 由于需要对shiro的SecurityManager进行设置,所以不能使用shiro-spring-boot-starter进行与springboot的整合,只能使用spring-shiro

    <!-- 自动依赖导入 shiro-core 和 shiro-web -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.1</version>
    </dependency>
    
  1. 由于需要实现无状态的web,所以使用不到Shiro的Session功能,严谨点就是将其关闭

    public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
    
        @Override
        public Subject createSubject(SubjectContext context) {
            // 不创建 session
            context.setSessionCreationEnabled(false);
            return super.createSubject(context);
        }
    }
    

    这样如果调用getSession()方法会抛出异常

4.1 流程

  1. 用户请求,不携带token,就在JwtFilter处抛出异常/返回没有登录,让它去登陆
  2. 用户请求,携带token,就到JwtFilter中获取jwt,封装成JwtToken对象。然后使用JwtRealm进行认证
  3. 在JwtRealm中进行认证判断这个token是否有效,也就是
执行流程:1. 客户端发起请求,shiro的过滤器生效,判断是否是login或logout的请求<br/>    如果是就直接执行请求<br/>    如果不是就进入JwtFilter2. JwtFilter执行流程    1. 获取header是否有"Authorization"的键,有就获取,没有就抛出异常    2. 将获取的jwt字符串封装在创建的JwtToken中,使用subject执行login()方法进行校验。这个方法会调用创建的JwtRealm    3. 执行JwtRealm中的认证方法,使用`jwtUtil.isVerify(jwt)`判断是否登录过    4. 返回true就使基础执行下去

4.2 快速开始

0. JwtDeafultSubjectFactory

package cn.coderymy.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不创建 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

1. 创建JwtUtil

这个一般是固定的写法,其中写了大量注释

package cn.coderymy.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/*
* 总的来说,工具类中有三个方法
* 获取JwtToken,获取JwtToken中封装的信息,判断JwtToken是否存在
* 1. encode(),参数是=签发人,存在时间,一些其他的信息=。返回值是JwtToken对应的字符串
* 2. decode(),参数是=JwtToken=。返回值是荷载部分的键值对
* 3. isVerify(),参数是=JwtToken=。返回值是这个JwtToken是否存在
* */
public class JwtUtil {
    //创建默认的秘钥和算法,供无参的构造方法使用
    private static final String defaultbase64EncodedSecretKey = "badbabe";
    private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

    public JwtUtil() {
        this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
    }

    private final String base64EncodedSecretKey;
    private final SignatureAlgorithm signatureAlgorithm;

    public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
        this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
        this.signatureAlgorithm = signatureAlgorithm;
    }

    /*
     *这里就是产生jwt字符串的地方
     * jwt字符串包括三个部分
     *  1. header
     *      -当前字符串的类型,一般都是“JWT”
     *      -哪种算法加密,“HS256”或者其他的加密算法
     *      所以一般都是固定的,没有什么变化
     *  2. payload
     *      一般有四个最常见的标准字段(下面有)
     *      iat:签发时间,也就是这个jwt什么时候生成的
     *      jti:JWT的唯一标识
     *      iss:签发人,一般都是username或者userId
     *      exp:过期时间
     *
     * */
    public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())//2. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
                .setIssuedAt(new Date(nowMillis))//1. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
                .setSubject(iss)//3. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    //相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
    //也就是拿到荷载部分所有的键值对
    public Claims decode(String jwtToken) {

        // 得到 DefaultJwtParser
        return Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(base64EncodedSecretKey)
                // 设置需要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判断jwtToken是否合法
    public boolean isVerify(String jwtToken) {
        //这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
        Algorithm algorithm = null;
        switch (signatureAlgorithm) {
            case HS256:
                algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                break;
            default:
                throw new RuntimeException("不支持该算法");
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(jwtToken);  // 校验不通过会抛出异常
        //判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
        return true;
    }

    public static void main(String[] args) {
        JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
        //以tom作为秘钥,以HS256加密
        Map<String, Object> map = new HashMap<>();
        map.put("username", "tom");
        map.put("password", "123456");
        map.put("age", 20);

        String jwtToken = util.encode("tom", 30000, map);

        System.out.println(jwtToken);
        util.decode(jwtToken).entrySet().forEach((entry) -> {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        });
    }
}

2. 创建JwtFilter

也就是在Shiro的拦截器中多加一个,等下需要在配置文件中注册这个过滤器

package cn.coderymy.filter;

import cn.coderymy.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/*
 * 自定义一个Filter,用来拦截所有的请求判断是否携带Token
 * isAccessAllowed()判断是否携带了有效的JwtToken
 * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
 * */
@Slf4j
public class JwtFilter extends AccessControlFilter {
    /*
     * 1. 返回true,shiro就直接允许访问url
     * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        log.warn("isAccessAllowed 方法被调用");
        //这里先让它始终返回false来使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回结果为true表明登录通过
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.warn("onAccessDenied 方法被调用");
        //这个地方和前端约定,要求前端将jwtToken放在请求的Header部分

        //所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        log.info("请求的 Header 中藏有 jwtToken {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定写法
         * */
        try {
            // 委托 realm 进行登录认证
            //所以这个地方最终还是调用JwtRealm进行的认证
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            e.printStackTrace();
            onLoginFail(servletResponse);
            //调用下面的方法向客户端返回错误信息
            return false;
        }

        return true;
        //执行方法中没有抛出异常就表示登录成功
    }

    //登录失败时默认返回 401 状态码
    private void onLoginFail(ServletResponse response) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("login error");
    }
}

3. 创建JwtToken

其中封装了需要传递的jwt字符串

package cn.coderymy.shiro;

import org.apache.shiro.authc.AuthenticationToken;

//这个就类似UsernamePasswordToken
public class JwtToken implements AuthenticationToken {

    private String jwt;

    public JwtToken(String jwt) {
        this.jwt = jwt;
    }

    @Override//类似是用户名
    public Object getPrincipal() {
        return jwt;
    }

    @Override//类似密码
    public Object getCredentials() {
        return jwt;
    }
    //返回的都是jwt
}

4. JwtRealm

创建判断jwt是否有效的认证方式的Realm

package cn.coderymy.realm;

import cn.coderymy.shiro.JwtToken;
import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@Slf4j
public class JwtRealm extends AuthorizingRealm {
    /*
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     * 不负责验证其他的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //这个token就是从过滤器中传入的jwtToken
        return token instanceof JwtToken;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    //这个token就是从过滤器中传入的jwtToken
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String jwt = (String) token.getPrincipal();
        if (jwt == null) {
            throw new NullPointerException("jwtToken 不允许为空");
        }
        //判断
        JwtUtil jwtUtil = new JwtUtil();
        if (!jwtUtil.isVerify(jwt)) {
            throw new UnknownAccountException();
        }
        //下面是验证这个user是否是真实存在的
        String username = (String) jwtUtil.decode(jwt).get("username");//判断数据库中username是否存在
        log.info("在使用token登录"+username);
        return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
        //这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名

    }

}

5. ShiroConfig

配置一些信息

  1. 因为不适用Session,所以为了防止会调用getSession()方法而产生错误,所以默认调用自定义的Subject方法
  2. 一些修改,关闭SHiroDao等
  3. 注册JwtFilter
package cn.coderymy.config;

import cn.coderymy.filter.JwtFilter;
import cn.coderymy.realm.JwtRealm;
import cn.coderymy.shiro.JwtDefaultSubjectFactory;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

//springBoot整合jwt实现认证有三个不一样的地方,对应下面abc
@Configuration
public class ShiroConfig {
    /*
     * a. 告诉shiro不要使用默认的DefaultSubject创建对象,因为不能创建Session
     * */
    @Bean
    public SubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }

    @Bean
    public Realm realm() {
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        /*
         * b
         * */
        // 关闭 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //禁止Subject的getSession方法
        securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        shiroFilter.setLoginUrl("/unauthenticated");
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        /*
         * c. 添加jwt过滤器,并在下面注册
         * 也就是将jwtFilter注册到shiro的Filter中
         * 指定除了login和logout之外的请求都先经过jwtFilter
         * */
        Map<String, Filter> filterMap = new HashMap<>();
        //这个地方其实另外两个filter可以不设置,默认就是
        filterMap.put("anon", new AnonymousFilter());
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("logout", new LogoutFilter());
        shiroFilter.setFilters(filterMap);

        // 拦截器
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/logout", "logout");
        filterRuleMap.put("/**", "jwt");
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

        return shiroFilter;
    }
}


6. 测试

package cn.coderymy.controller;

import cn.coderymy.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class LoginController {

    @RequestMapping("/login")
    public ResponseEntity<Map<String, String>> login(String username, String password) {
        log.info("username:{},password:{}",username,password);
        Map<String, String> map = new HashMap<>();
        if (!"tom".equals(username) || !"123".equals(password)) {
            map.put("msg", "用户名密码错误");
            return ResponseEntity.ok(map);
        }
        JwtUtil jwtUtil = new JwtUtil();
        Map<String, Object> chaim = new HashMap<>();
        chaim.put("username", username);
        String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
        map.put("msg", "登录成功");
        map.put("token", jwtToken);
        return ResponseEntity.ok(map);
    }
    @RequestMapping("/testdemo")
    public ResponseEntity<String> testdemo() {
        return ResponseEntity.ok("我爱蛋炒饭");
    }

}

4.3 授权方面的信息

在JwtRealm中的授权部分,可以使用JwtUtil.decode(jwt).get("username")获取到username,使用username去数据库中查找到对应的权限,然后将权限赋值给这个用户就可以实现权限的认证了

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

推荐阅读更多精彩内容