Springboot shiro JWT集成总结

SpringBoot Shiro JWT

1.建表

DDL.sql

CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(30) NOT NULL DEFAULT '',
  `password` varchar(32) NOT NULL DEFAULT '',
  `salt` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

CREATE TABLE `t_sys_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `role_name` varchar(20) DEFAULT NULL COMMENT '角色名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

CREATE TABLE `t_sys_permission` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '权限名称解释',
  `url` varchar(60) DEFAULT NULL COMMENT '权限',
  `permission` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

CREATE TABLE `t_user_role` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `role_id` bigint(11) DEFAULT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色对应表';

CREATE TABLE `t_sys_permission` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '权限名称解释',
  `permission` varchar(30) DEFAULT NULL,
  `url` varchar(60) DEFAULT NULL COMMENT '权限',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

DML.sql

-- 创建角色
INSERT INTO `t_sys_role` VALUES (null, '管理员');
INSERT INTO `t_sys_role` VALUES (null, '普通用户');

-- 创建用户
INSERT INTO `t_user` VALUES (null, 'admin', '856aea89ad509f163284abb75579dcfc', 'admin');
INSERT INTO `t_user` VALUES (null, 'user', 'ea6151b14a3b64331d6c33e645fccef6', 'user');

-- 创建权限
INSERT INTO `t_sys_permission` VALUE(null, '超级用户权限', 'root:all', '/**');
INSERT INTO `t_sys_permission` VALUE(null, '查看用户列表', 'user:list', '/admin/userList');

-- 给用户配置角色
INSERT INTO `t_user_role` VALUE(null, 14, 1);
INSERT INTO `t_user_role` VALUE(null, 15, 2);

-- 给角色配置权限
INSERT INTO `t_role_permission` VALUE(null, 1, 1);
INSERT INTO `t_role_permission` VALUE(null, 2, 2);

4. springboot 项目加入shiro依赖

		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring-boot-starter</artifactId>
			<version>1.4.0</version>
		</dependency>

		<!-- 这个用于开启缓存功能 -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>1.4.0</version>
		</dependency>

		<!-- jwt jar-->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.2.0</version>
		</dependency>

5. 配置shiro

package com.grady.shirodemo.config;

import com.grady.shirodemo.filter.JWTFilter;
import com.grady.shirodemo.realm.JWTRealm;
import com.grady.shirodemo.realm.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.servlet.Filter;
import java.util.*;

@Configuration
public class ShiroConfig {

  	/**
  	* 最重要的一个bean
  	**/
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

      	// 加入自定义的拦截器,会拦截(所有被“jwt”标记的方法)请求到url访问的方法
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置未登录跳转到的url
        shiroFilterFactoryBean.setLoginUrl("/toLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorize");

        // 设置拦截器与url映射关系map
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/unAuthorize", "anon");
        // 所有请求通过我们自己的JWT Filter
      	// 注意越先设置的url会越先生效,原因你懂的(LinkedHashMap 有先后顺序)
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

  	// 这里是配置密码转换逻辑matcher(此处会把传进来的密码以MD5的算法序列化5次)
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(5);
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
    }

    // 核心bean,所有的realm都要在这里加进去
    @Bean
    public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setAuthenticator(modularRealmAuthenticator());
        // realm只能在你setAuthenticator下设置入securityManager,否则不生效
        List<Realm> list = new ArrayList<>();
        list.add(userRealm());
        list.add(jwtRealm());
        securityManager.setRealms(list);
        // 这里虽是方法调用,但是引用地址都是一样的,所以是同一个对象
        securityManager.setCacheManager(ehCacheManager());

        // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

  	// 这是用账号密码来校验的realm
    @Bean
    public UserRealm userRealm() {
        UserRealm realm = new UserRealm();
        //启用授权缓存
        realm.setAuthorizationCachingEnabled(true);
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

  	// 这是通过token来校验的realm
    @Bean
    public JWTRealm jwtRealm() {
        return new JWTRealm();
    }

    /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 配置异常处理器,无权限时会走这里
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver resolver() {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/unAuthorize");
        resolver.setExceptionMappings(properties);
        return resolver;
    }

    /**
     * shiro缓存管理器;
     * 需要添加到securityManager中
     * @return
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
        return cacheManager;
    }

    /**
     * 系统自带的Realm管理,主要针对多realm
     * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
        // 这里配置多个realm只要一个通过就算通过(会串型执行)
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }
}

6. 完成两个realm的编写

UserRealm.java

package com.grady.shirodemo.realm;

import com.grady.shirodemo.entity.model.User;
import com.grady.shirodemo.entity.model.UserPermission;
import com.grady.shirodemo.entity.model.UserRole;
import com.grady.shirodemo.service.PermissionService;
import com.grady.shirodemo.service.UserService;
import org.apache.shiro.authc.*;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class UserRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    @Autowired
    PermissionService permissionService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    /**
     * 用于授权,进行权限校验
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("UserRealm : doGetAuthorizationInfo 鉴权");
        // 这里即使抛出runtime异常也会被shiro Advicefilter 捕获
        User user = (User) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        List<UserRole> roleList = permissionService.findRolesByUserId(user.getId());
        Optional.ofNullable(roleList).ifPresent(roles -> {
            authorizationInfo.addRoles(roles.stream().map(UserRole::getRoleName).collect(Collectors.toList()));
            List<String> permList = new ArrayList<>();
            roles.forEach(role -> {
                // 这里即使抛出runtime异常也会被shiro Advicefilter 捕获
                List<UserPermission> permissions = permissionService.findPermissionByRoleId(role.getId());
                Optional.ofNullable(permissions).ifPresent(perms -> {
                    permList.addAll(perms.stream().map(UserPermission::getPermission).collect(Collectors.toList()));
                });
            });
            authorizationInfo.addStringPermissions(permList);
        });
        return authorizationInfo;
    }

    /**
     * 进行身份认证,即登录
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("UserRealm : doGetAuthenticationInfo 认证");
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        User user = Optional.ofNullable(userService.findUserByUserName(token.getUsername()))
                .orElseThrow(() -> new UnknownAccountException("无此用户"));
        //用用户名作为唯一盐值,确保序列化后的MD5值唯一
        ByteSource salt = ByteSource.Util.bytes(user.getUserName());
        AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
        return info;
    }
}

JWTRealm.java

package com.grady.shirodemo.realm;

import com.grady.shirodemo.entity.model.User;
import com.grady.shirodemo.jwt.JWTToken;
import com.grady.shirodemo.service.PermissionService;
import com.grady.shirodemo.service.UserService;
import com.grady.shirodemo.utils.JWTUtil;
import org.apache.shiro.authc.*;
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 java.util.Optional;

public class JWTRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    @Autowired
    PermissionService permissionService;

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

    /**
     * 用于授权,进行权限校验
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("JWTRealm : doGetAuthorizationInfo 鉴权");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        System.out.println("JWTRealm : doGetAuthenticationInfo 认证");
        String token = (String) auth.getCredentials();
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        User user = Optional.ofNullable(userService.findUserByUserName(username))
                .orElseThrow(() -> new UnknownAccountException("无此用户"));
        if (!JWTUtil.verify(token, username, user.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }
        return new SimpleAuthenticationInfo(user, token, getName());
    }
}

7. 配置Filter

package com.grady.shirodemo.filter;

import com.grady.shirodemo.jwt.JWTToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;

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

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private static final String TOKEN = "token";

    /**
     * 判断是否允许访问
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
              	// 如果responseUnAuthorize 重定向的url也是要被拦截的话会陷入死循话
                responseUnAuthorize(request, response);
                return false;
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader(TOKEN);
        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

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

    private void responseUnAuthorize(ServletRequest request, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/unAuthorize");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8. token 相关

JWToken

package com.grady.shirodemo.jwt;

import org.apache.shiro.authc.AuthenticationToken;

public class JWTToken implements AuthenticationToken {

    /**
     * 密钥
     */
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

JWTUtil

package com.grady.shirodemo.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.io.UnsupportedEncodingException;
import java.util.Date;

public class JWTUtil {

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

    private static final String CLAIM_STR = "username";
    /**
     * 校验token是否正确
     *
     * @param token    密钥
     * @param username 登录名
     * @param password 密码
     * @return
     */
    public static boolean verify(String token, String username, String password) {
        try {
            // 这里与密码无关,是生成(或确认)token的算法
            Algorithm algorithm = Algorithm.HMAC256(password);
            JWTVerifier verifier = JWT.require(algorithm).withClaim(CLAIM_STR, username).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取登录名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(CLAIM_STR).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成token签名,指定时间后过期,一经生成不可修改,令牌在指定时间内一直有效
     * @param userNo 用户编号
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String userNo, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            // 这里与密码无关,是生成token的算法
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            return JWT.create()
                    .withClaim(CLAIM_STR, userNo)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

posted @ 2022-04-16 13:56  明月照江江  阅读(145)  评论(0编辑  收藏  举报