SpringBoot+Shiro+Jwt实现登陆认证——最干的干货

 1. 概述

1.1 SpringBoot

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

1.2 Shiro

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

能够实现用户的认证和受权。比SpringSecurity要简单的多。web

1.3 Jwt

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

为何不适用Session?spring

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

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

它使用的是一种令牌技术apache

Jwt字符串分为三部分json

  1. Header安全

    存储两个变量springboot

    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;
    }
}

 

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());
            });
        }
    }

    解析:

    1. 在建立JwtUtil对象的时候须要传入几个数值
      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>
  2. 因为须要实现无状态的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去数据库中查找到对应的权限,而后将权限赋值给这个用户就能够实现权限的认证了

相关文章
相关标签/搜索