springboot整合shiro+JWT

必备知识

shiro执行过程

user实体

package com.dahuici.zyb.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.sun.xml.internal.ws.developer.Serialization;
import lombok.Data;

import java.io.Serializable;

@Data
public class User{
    private String name;
    private Integer age;
    //@TableId("userid")
    private Integer userid;
    private String password;

    public User(String name,String password){
        this.name = name;
        this.password = password;
    }

    public User(){

    }

}

自定义Realm如下

package com.dahuici.zyb.authentication;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dahuici.zyb.Dao.UserDao;
import com.dahuici.zyb.entity.User;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;


public class MyRealm extends AuthorizingRealm {
    private  Map<String,User> users = new HashMap<>();
    private  Map<String,List<String>> roles = new HashMap<>();;
    private  Map<String,List<String>> permissions = new HashMap<>();;

    {
        users.put("老板",new User("老板","123456"));
        users.put("总监",new User("总监","123456"));
        users.put("普通员工",new User("普通员工","123456"));

        List<String> role1 = new ArrayList<>();
        List<String> role2 = new ArrayList<>();
        List<String> role3 = new ArrayList<>();
        role1.add("企业最高管理者");
        role1.add("部门管理者");
        role1.add("普通管理者");

        role2.add("部门管理者");
        role2.add("普通管理者");

        role3.add("普通管理者");

        roles.put("老板",role1);
        roles.put("总监",role2);
        roles.put("普通员工",role3);

        List<String> permission1 = new ArrayList<>();
        List<String> permission2 = new ArrayList<>();
        List<String> permission3= new ArrayList<>();

        permission1.add("涨工资");
        permission1.add("招人");
        permission1.add("工作");

        permission2.add("招人");
        permission2.add("工作");

        permission3.add("工作");

        permissions.put("企业最高管理者",permission1);
        permissions.put("部门管理者",permission2);
        permissions.put("普通管理者",permission3);


    }

    /*@Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }*/


    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取通过AuthenticationInfo传过来的user
        User user = (User)principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //模仿从数据库获取该用户角色集合
        List<String> userRoles = getUserRole(user.getName());
        //将该用户的角色集合添加进simpleAuthorizationInfo
        simpleAuthorizationInfo.addRoles(userRoles);

        //模仿从数据库获取该用户权限集合
        List<String> list = new ArrayList<>();
        for (String rolename:userRoles) {
            list.addAll(getUserPermission(rolename));
        }
        //权限去重
        ArrayList<String> userPermissions = new ArrayList<>(new HashSet<>(list));
        //将该用户的权限集合添加进simpleAuthorizationInfo
        simpleAuthorizationInfo.addStringPermissions(userPermissions);
        System.err.println(userPermissions);
        return simpleAuthorizationInfo;
    }

    //登录验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从token中获取用户名
        String username = (String)authenticationToken.getPrincipal();

        //根据用户名获取用户信息(企业中这一步都是查询数据库,这里只是模拟了数据库功能)
        User user = getUserByName(username);

        if (user == null){
            throw new AuthenticationException("用户不存在");
        }

        //返回带有用户名与密码的AuthenticationInfo
        AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), "myRealm");
        return info;
    }

    //通过用户名获取用户信息
    protected User getUserByName(String username){
        return users.get(username);
    }

    //获取指定用户的角色
    protected List<String> getUserRole(String username){
        return roles.get(username);
    }

    //获取指定角色的权限
    protected List<String> getUserPermission(String rolename){
        return permissions.get(rolename);
    }

}

执行测试代码如下:

@Test
    public void testAuthentication() {

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //设置Realm(负责提供数据);
        MyRealm myRealm = new MyRealm();
        defaultSecurityManager.setRealm(myRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体
        //创建token
        UsernamePasswordToken token = new UsernamePasswordToken("老板", "123456");


        subject.login(token); // 登录,密码或者用户名验证失败,抛出异常

        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 已认证subject.isAuthenticated()返回true
        //角色验证,判断该登陆者是否有该角色,验证成功,无异常,验证失败,抛出异常
        subject.checkRole("企业最高管理者");
        //权限验证,判断该登陆者是否有该权限,验证成功,无异常,验证失败,抛出异常
        subject.checkPermission("涨工资");

        subject.logout(); // 登出

        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 登出,subject.isAuthenticated()返回false
    }

主要对象(概念可以百度,官方解释很多,这里只大概说一下它们的关系)
SecurityManager:安全环境对象,需要配置Realm对象来获取数据。这里使用的是其实现类DefaultSecurityManager对象。
Realm:个人理解就是一个获取数据并保存数据的一个对象,主要使用两个方法,一个登陆验证,一个授权验证。这里是自定义的Realm,继承了AuthorizingRealm类。shiro通过自定义Realm的方式将获取用户密码,角色,权限交给我们自己来设置,它只负责对其进行使用,这样就让操作更加的灵活。
SecurityUtils:获取Subject的工具,需要给其配置安全环境(SecurityManager),其才能根据安全环境获取对应的Subject。
Subject:用户对象(代表任意交互对象),登陆与授权都有该对象完成。

传递信息的对象:
AuthenticationToken
AuthenticationInfo
PrincipalCollection
AuthorizationInfo

这些传递消息对象的传递过程大概如下:


传递过程.png

图片上的过程简单来说就是subject.login(token)传入一个token,这个token会作为自定义Realm的doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法的实参传入,最后返回一个AuthenticationInfo实例,AuthenticationInfo有个PrincipalCollection属性用于将参数传递给doGetAuthorizationInfo(PrincipalCollection principalCollection)方法,最后doGetAuthorizationInfo(PrincipalCollection principalCollection)返回一个该封装了该用户角色与权限信息的AuthorizationInfo。

根据传递过程浅谈Realm的两个方法和对消息传递对象的使用。
doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法

登陆方法.png

该方法在登录验证的时候自动调用。登录方法的参数authenticationToken就是subject.login(token)里面的token,我们通过该token获取传入的用户名,用该用户名查出用户密码信息。如果该用户不存在,返回null,如果存在,将该用户的密码封装在AuthenticationInfo中返回,用于shiro判断密码是否正确。需要注意的是,AuthenticationInfo第一个参数是用于传递给授权方法使用的,与登录验证基本没有关系,不用和登录的token参数保持一致。简单来说,登录方法的原理就是通过用户名获取到用户过后,代表用户名验证成功,将用户密码返回用于密码验证(这一步是shiro内部完成),所以就不必保持传入AuthenticationToken的第一个参数与AuthenticationInfo的第一个参数保持一致了,因为通过用户名没有获取到用户密码这些信息就已经表示用户名验证失败了。需要注意的是AuthenticationInfo的第一个参数虽然可以传任意对象,但是该对象必须对获取该用户的角色与权限有帮助,这是第一个参数最主要的作用。
doGetAuthorizationInfo(PrincipalCollection principalCollection)方法
授权.png

该方法是在角色与权限验证的时候自动调用的,需要注意的是,验证是前提是已经登录。授权方法的参数PrincipalCollection其实就是登录验证方法返回对象AuthenticationInfo的属性(new的时候传入的第一个参数),通过该属性获取到用户名用于查权限与角色信息,封装在AuthorizationInfo中用于角色与权限验证。

JWT

JWT主要由三部分组成: Header - 头部 、Payload - 负载 、Signature(签名)
即生成的token格式为:Header.Payload.Signature
个人理解JWT就是一个生成并解析令牌的一个工具。我们可以将令牌传给前端,然后每次前端用该令牌进行身份验证。

依赖:

<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>

工具类

package com.dahuici.adc.common.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

public class JWTUtil {

    // 过期时间
    private static final long EXPIRE_TIME = 30*60*1000;//

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码(最好加密过)
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            //使用HMAC250算法加密密钥
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,30min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {

        Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
        //密钥和加密算法
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);

    }
    
}

自定义Filer(重写内置BasicHttpAuthenticationFilter过滤器)

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter的主要方法:

1 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
预处理,进行验证之前执行的方法,可以理解为该过滤器最先执行的方法。该方法执行后执行isAccessAllowed方法。

2 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
该方法用于判断是否登录,BasicHttpAuthenticationFilter底层是通过subject.isAuthenticated()方法判断的是否登录的。
该方法返回值:
如果未登录,返回false, 进入onAccessDenied
如果登录了,返回true, 允许访问,不用继续验证,可以访问接口获取数据。

3 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception
判断是否拒绝访问。个人理解就是当用户没有登录访问该过滤器的过滤的接口时,就必须进行httpBasic验证。
在该方法内部,先使用isLoginAttempt方法判断是否登录模式。
如果是登录模式,就执行executeLogin方法。反之,执行sendChallenge
该方法返回值:
如果执行了executeLogin并登陆成功,返回true,允许访问。
如果执行executeLogin但没登陆成功或者执行了sendChallenge,返回false,拒绝访问。

4 protected boolean isLoginAttempt(ServletRequest request, ServletResponse response)
是否是登录模式
判断依据,是否携带header(Authorization)。
该方法返回值:
该方法返回true,携带 是登录模式 。
返回false,该方法不是登录模式。

5 protected boolean sendChallenge(ServletRequest request, ServletResponse response)
该方法主要作用是,在请求没有携带header(Authorization)时,添加响应头WWW-Authenticate进行httpBasc验证。
该方法返回值:
只有fasle。
浏览器接收到含有WWW-Authenticate的响应头会弹出输入用户名与密码的输入框。

6 protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception
在请求携带header(Authorization),通过获取请求头Authorization的获取username与password创建token。
然后调用subject.login(token)使用该token进行登录验证。
格式:Authorization: Basic 6ICB5p2/OjEyMzQ1Ng==
该方法返回值:
true 登陆成功。
false 登陆失败。

方法执行流程.png

只有大致了解org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter主要方法的功能和执行流程,才能根据需求对其进行覆盖。

开始springboot整合shiro+ JWT

简单讲述一下功能:要访问系统的接口,除了登陆接口外,访问其他接口必须登陆或者携带了正确的token才能访问。

1 导入相关依赖

使用shiro做一个简单的需求,用户不登录,不能访问其他的接口获取数据。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>springmybatisplus</groupId>
    <artifactId>springmybatisplus</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--引入springboot的父工程,引入就表示这是一个springboot项目了,引入该依赖后,常用的相关依赖包不用指定版本-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
    </dependencies>

</project>

2 编写自定义的MyRealm与自定义过滤器

在自定义Realm的时候,需要注意的是AuthorizingRealm只支持UsernamePasswordToken类型的token,传入其他token会报错,如果需要自定义的Realm支持其他token(例如自定义的token),需要重写如下方法:

public boolean supports(AuthenticationToken token)

Realm就是通过supports方法判断是否支持token的。

package com.dahuici.zyb.authentication;

import com.dahuici.zyb.entity.User;
import com.dahuici.zyb.utils.JWTUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.*;


public class MyRealm extends AuthorizingRealm {
    private  Map<String,User> users = new HashMap<>();
    private  Map<String,List<String>> roles = new HashMap<>();;
    private  Map<String,List<String>> permissions = new HashMap<>();;

    {
        users.put("老板",new User("老板","123456"));
        users.put("总监",new User("总监","123456"));
        users.put("普通员工",new User("普通员工","123456"));

        List<String> role1 = new ArrayList<>();
        List<String> role2 = new ArrayList<>();
        List<String> role3 = new ArrayList<>();
        role1.add("企业最高管理者");
        role1.add("部门管理者");
        role1.add("普通管理者");

        role2.add("部门管理者");
        role2.add("普通管理者");

        role3.add("普通管理者");

        roles.put("老板",role1);
        roles.put("总监",role2);
        roles.put("普通员工",role3);

        List<String> permission1 = new ArrayList<>();
        List<String> permission2 = new ArrayList<>();
        List<String> permission3= new ArrayList<>();

        permission1.add("涨工资");
        permission1.add("招人");
        permission1.add("工作");

        permission2.add("招人");
        permission2.add("工作");

        permission3.add("工作");

        permissions.put("企业最高管理者",permission1);
        permissions.put("部门管理者",permission2);
        permissions.put("普通管理者",permission3);


    }


    //授权,该方法的作用就是查出用户所有角色与权限信息,并添加进simpleAuthorizationInfo对象里面。
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取通过AuthenticationInfo传过来的user
        User user = (User)principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //模仿从数据库获取该用户角色集合
        List<String> userRoles = getUserRole(user.getName());
        //将该用户的角色集合添加进simpleAuthorizationInfo
        simpleAuthorizationInfo.addRoles(userRoles);

        //模仿从数据库获取该用户权限集合
        List<String> list = new ArrayList<>();
        for (String rolename:userRoles) {
            list.addAll(getUserPermission(rolename));
        }
        //权限去重
        ArrayList<String> userPermissions = new ArrayList<>(new HashSet<>(list));
        //将该用户的权限集合添加进simpleAuthorizationInfo
        simpleAuthorizationInfo.addStringPermissions(userPermissions);
        //原理都是获取用户的角色与权限字符串集合,然后添加进simpleAuthorizationInfo里面。
        return simpleAuthorizationInfo;
    }

    //登录验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从token中获取用户名
        String token = (String)authenticationToken.getPrincipal();
        //冲token中获取用户名
        String username = JWTUtils.getUsername(token);

        //根据用户名获取用户信息(企业中这一步都是查询数据库,这里只是模拟了数据库功能)
        User user = getUserByName(username);
        if (user == null){
            throw new AuthenticationException("用户不存在");
        }

        boolean result = JWTUtils.verify(token, user.getName(), user.getPassword());
        //由于使用的是JWT,所以密码需要自己验证而不是交给shiro去验证。
        if(!result){
            throw new AuthenticationException("用户密码错误");
        }

        //返回带有用户名与密码的AuthenticationInfo
        AuthenticationInfo info = new SimpleAuthenticationInfo(user, token, "myRealm");
        return info;
    }

    //通过用户名获取用户信息
    protected User getUserByName(String username){
        return users.get(username);
    }

    //获取指定用户的角色
    protected List<String> getUserRole(String username){
        return roles.get(username);
    }

    //获取指定角色的权限
    protected List<String> getUserPermission(String rolename){
        return permissions.get(rolename);
    }




}


需要用到的User实体类

package com.dahuici.zyb.entity;

import lombok.Data;

@Data
public class User{
    private Integer userid;
    private String name;
    private String password;

    public User(String name,String password){
        this.name = name;
        this.password = password;
    }

    public User(){

    }

}

自定义的filter(该过滤器只做登录验证,不做权限验证)

package com.dahuici.zyb.filter;

import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class MyShiroFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        System.err.println("preHandle");
        return super.preHandle(request, response);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        System.err.println("isAccessAllowed");
        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    //返回true,允许访问,返回false,拒绝访问。
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        System.err.println("onAccessDenied");
        try {
            if (isLoginAttempt(request,response)){
                return executeLogin(request,response);//携带了header--Authorization
            }else{
                //如果没有携带header--Authorization,这里直接提示,
                // 屏蔽掉了原来的执行sendChallenge
                //还有这个提示方式还可以改进,比如使用自定义的一些ResponseEntity什么的,
                //这里只简单的提示一下
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json; charset=utf-8");
                response.getWriter().write("请完成认证,要么登陆,要么携带正确的token");
                return false;
            }
        }catch (Exception e){
            //捕获使用JWTtoken登陆过程出现的异常,输出错误信息
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(e.getMessage());
            return false;
        }



    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization");
        return token != null;
    }

    @Override
    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
        System.err.println("sendChallenge");
        return super.sendChallenge(request, response);
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        UsernamePasswordToken token = new UsernamePasswordToken(authorization, authorization);
        getSubject(request,response).login(token);
        return true;
    }

}

JWT工具

package com.dahuici.zyb.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

public class JWTUtils {

    // 过期时间
    private static final long EXPIRE_TIME = 30*60*1000;



    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码(最好加密过)
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,30min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {

        Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
        //密钥和加密算法
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);

    }

}

3 将shiro相关对象交于spring管理并配置过滤器

package com.dahuici.zyb.config;

import com.dahuici.zyb.authentication.MyRealm;
import com.dahuici.zyb.filter.MyShiroFilter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
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.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(){
        //需要注意这里是DefaultWebSecurityManager,该对象是专门用于整合springboot使用的SecurityManager,
        // 而不是使用DefaultSecurityManager
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给该对象封装好MyRealm
        MyRealm myRealm = new MyRealm();
        defaultWebSecurityManager.setRealm(myRealm);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        //封装ShiroFilter的过滤器工厂
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("myfilter", new MyShiroFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //配置路径与该路径使用的过滤器
        HashMap<String, String> filteDefinitionrMap = new HashMap<>();
        filteDefinitionrMap.put("/login","anon");//不用登陆验证就能访问
        filteDefinitionrMap.put("/**","myfilter");//必须登录后才能访问

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filteDefinitionrMap);
        //除了使用shiro内置的服务器,还可以自定义过滤器,然后通过下面的方法添加进去。
        //shiroFilterFactoryBean.setFilters(map);
        return shiroFilterFactoryBean;

    }

}

想到使用登录验证,当然必须要使用过滤器了,而shiro为整合spring提供了自己的过滤器工厂,并且shiro也提供了很多的内置过滤器供我们使用,在使用内置过滤器的同时也可以自定义自己的过滤器,非常灵活。

shiro内置过滤器如下:
内置过滤器都在org.apache.shiro.web.filter.mgt.DefaultFilter枚举下面。
anon(不用登陆就能访问) org.apache.shiro.web.filter.authc.AnonymousFilter
authc(必须登录才能访问) org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic(http基本验证) org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout(退出) org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation(不创建session) org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms(需要权限验证才能访问) org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port(端口验证) org.apache.shiro.web.filter.authz.PortFilter
rest (rest方面) org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles(需要角色验证才能访问) org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl (ssl方面) org.apache.shiro.web.filter.authz.SslFilter
user (用户方面) org.apache.shiro.web.filter.authc.UserFilter

4 Controller

package com.dahuici.zyb.controller;



import com.dahuici.zyb.service.UserService;
import com.dahuici.zyb.utils.JWTUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;


@RestController
@RequestMapping
public class MyTestController {



    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public String login(String name, String password, HttpServletResponse response){
        Subject subject = SecurityUtils.getSubject();
        //使用用户名与密码生成token(注意,一般密码都要加密,
        // 加密后保证数据库保存的密码一致就行了,由于这里是演示就没有加密)
        String token = JWTUtils.sign(name, password);
        //注意,由于用户名与密码都在token里面,这里传入的用户名都是token。
        UsernamePasswordToken authenToken = new UsernamePasswordToken(token,token);
        try {
            subject.login(authenToken);
            //经token放在请求头里面
            response.setHeader("Authorization", token);
        }catch(Exception e){
            return e.getMessage();
        }

        return "had login";
    }

    @GetMapping("/isLogin")
    public String isLogin(){
        return "isLogin";
    }

}

5 访问测试

先使用/IsLogin接口访问


image.png

访问/login接口再访问/IsLogin


image.png
image.png

获取登陆后的token用postman访问


image.png

image.png

这里可能有人觉得登陆验证过后,还要用token验证有点多余,可以吧登录验证与token验证合并一下。这里只是为了演示一下,就使用了双重验证,但是,只要理解了BasicHttpAuthenticationFilter几个主要方法的关系,就能根据自己的需求实现自定义的Filter,比如讲登陆验证与token验证合并,就可以通过重写isAccessAllowed实现。

启用注解支持

1 需要添加如下两个bean

/**
     * 下面的代码是添加注解支持
     */

    //
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

2 添加权限控制的异常捕获
专门捕获shiro权限验证失败时抛出的异常


@ControllerAdvice
@Slf4j
public class MyAuthorizationExceptionHandler {

    @ExceptionHandler(AuthorizationException.class)
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {

        return "没有通过角色验证!";
    }

}

3 给接口添加验证
这里使用了角色验证,用户必须拥有admin角色才能访问该接口。

@GetMapping("/getSomething")
    //企业最高管理者
    @RequiresRoles(value = {"admin"})
    public String getSomething(){
        return "success";
    }

以上就是对springboot整合shiro+jwt的最基本的入门级使用过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容