SpringBoot+Shiro+JWT

SpringBoot+Shiro+JWT

一、Shiro

1、什么是shiro?

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的

API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架

应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。

2、shiro能做什么?

  • 验证用户来核实他们的身份。
  • 对用户执行访问控制。
  • 判断用户是否被分配了一个确定的安全角色。
  • 判断用户是否被允许做某事。
  • 在任何环境下使用 Session API,即使没有 Web 或 EJB 容器。
  • 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应。
  • 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”。
  • 启用单点登录(SSO)功能。
  • 为没有关联到登录的用户启用"Remember Me"服务。

3、shiro有哪些功能模块?

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份。

  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。

  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话

    中;会话可以是普通JavaSE环境的,也可以是如Web环境的。

  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。

  • Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。

  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。

  • Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。

  • Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。

  • "Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。

  • "Remember Me":记住我。

4、shiro的内部结构是什么样的?

  • Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

  • SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实

    现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

  • Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

  • SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所以,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;

  • SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的

  • Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

5、搭建一个shiro应用需要做什么?

1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判

断。

二、JWT

1、什么是JWT?

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

2、JWT的请求流程?

  1. 用户使用账号和密码发出post请求;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器。

3、JWT适合的场景?

身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

4、JWT有哪些优点?

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
  • 不需要在服务端保存会话信息,特别适用于分布式微服务。

5、JWT的结构?

Jwt包含三部分

1、Header 头部

JWT的头部承载两部分信息:token类型和采用的加密算法。

{ 
  "alg": "HS256",
  "typ": "JWT"
} 

声明加密的算法:通常直接使用 HMAC SHA256

声明类型:这里是jwt

加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。

  • MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。不管文件多大,经过MD5后都能生成唯一的MD5值
  • SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
  • HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
2、Payload 负载

载荷就是存放有效信息的地方。

有效信息包含三个部分

1.标准中注册的声明

建议但不强制使用:

  • iss: jwt签发者
  • sub: 面向的用户(jwt所面向的用户)
  • aud: 接收jwt的一方
  • exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2.公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。

3.私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

 public static String createToken(String msg) {
        try {
            final Algorithm signer = Algorithm.HMAC256("私钥");
            return JWT.create()
                    .withIssuer("jwt签发者")
                    .withSubject("面向的用户(jwt所面向的用户)")
                    .withAudience("接收jwt的一方")
                    .withClaim("放置JWT中的数据", msg)
                    .withExpiresAt(new Date())
                    .withNotBefore(new Date())
                    .withIssuedAt(new Date())
                    .withJWTId("jwtID")
                    .sign(signer);
        } catch (Exception e) {
            logger.warn("生成JWT失败!{}", e.getMessage());
            return null;
        }
 }
3、Signature 签名/签证

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(私钥)

这个部分需要base64加密后的header和base64加密后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

三、SpringBoot+Shiro+JWT Demo

新建一个SpringBoot项目

1、引入依赖


  com.auth0
  java-jwt
  3.4.1


  org.apache.shiro
  shiro-spring
  1.7.1

2、JWT Filter

封装一个返回体:

/**
 * REST 服务标准 返回类型
 * @author Administrator
 *
 */
public class Result {
    
    public Result(int code){
        setCode(code);
    }
    
    /**
     * 代码
     */
    private int code;
    private int status;
    private String serverTime =new Date();
    private String requestId;
    private String exception;
    /**
     * 错误信息(提交失败)
     */
    private BizError error;
    
    private T data;
    /**
     * 消息信息
     */
    private String message;
    
    /**
     * 校验参数错误信息
     */
    private List invalidArgs;
}

JWT Filter:

/**
 * JWT认证过滤
 *
 * @Author liao xiangdong
 * @Date 2021/8/4 14:31
 */
@Component
public class JWTFilter extends BasicHttpAuthenticationFilter {
    private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);

    /**
     * 判断是否已认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = SecurityUtils.getSubject();
        return null != subject && subject.isAuthenticated();
    }

    /**
     * 认证检查未通过执行该方法
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
        //1.检查请求头中是否含有token
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("token");
        //2.如果客户端没有携带token,拦下请求
        if (null==token || "".equals(token)) {
            responseTokenError(response, "token无效,您无权访问该接口");
            return false;
        }
        //3.如果有,对token进行验证
        JWTToken jwtToken = new JWTToken(token);
        try {
            SecurityUtils.getSubject().login(jwtToken);
        } catch (AuthenticationException e) {
            logger.info("token验证失败!{}", e.getMessage());
            responseTokenError(response, e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 对跨域提供支持
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 返回token认证错误
     *
     * @param response
     * @param msg
     */
    private void responseTokenError(ServletResponse response, String msg) {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()) {
            ObjectMapper objectMapper = new ObjectMapper();
            Result result = new Result<>(5001);
            result.setMessage(msg);
            String content = objectMapper.writeValueAsString(result);
            out.append(content);
        } catch (IOException e) {
            logger.error("返回token认证错误信息失败!{}", e.getMessage());
        }
    }
}

3、JWT

/**
 * JWT Token
 *
 * @Author liao xiangdong
 * @Date 2021/8/5 10:12
 */
public class JWTToken implements AuthenticationToken {

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

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

    public String getToken() {
        return this.token;
    }

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

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

4、JWT Util

public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);


    /**
     * 加密生成token
     * @param username 用户名
     * @param role  用户角色
     * @return token
     */
     public static String createToken(String msg) {
        try {
            final Algorithm signer = Algorithm.HMAC256("私钥");
            return JWT.create()
                    .withIssuer("jwt签发者")
                    .withSubject("面向的用户(jwt所面向的用户)")
                    .withAudience("接收jwt的一方")
                    .withClaim("放置JWT中的数据", msg)
                    .withExpiresAt(new Date())
                    .withNotBefore(new Date())
                    .withIssuedAt(new Date())
                    .withJWTId("jwtID")
                    .sign(signer);
        } catch (Exception e) {
            logger.warn("生成JWT失败!{}", e.getMessage());
            return null;
        }
 }

    /**
     * 解析验证token
     * @param token 加密后的token字符串
     * @return 验证是否通过
     */
    public static Boolean verifyToken(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256("私钥");
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
            return true;
        } catch (Exception e) {
            logger.warn("校验JWT失败!{}",e.getMessage());
        }
        return false;
    }

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

5、自定义Realm

/**
 * 自定义realm
 *
 * @Author liao xiangdong
 * @Date 2021/8/4 14:40
 */
public class UserRealm extends AuthorizingRealm {


    private UserService userService;

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

    /**
     * 鉴权
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User activeUser = (User) SecurityUtils.getSubject().getPrincipal();
        authorizationInfo.addStringPermission(activeUser.getRole());
        return authorizationInfo;
    }

    /**
     * 根据token进行登陆验证
     *
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        //解决空对象
        preHandleNull();
        String token = (String) auth.getCredentials();
        //获得token中的username
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token错误,请重新登入!");
        }
        User user = userService.findUserByName(username);
        if (user == null) {
            throw new AccountException("账号不存在!");
        }
        //判断redis中存入的token是否过期
        if (userService.isExpire(username, token)) {
            throw new AuthenticationException("token过期,请重新登入!");
        }
        if (!JWTUtil.verifyToken(token)) {
            throw new AuthenticationException("token错误,请重新登入!");
        }
        if (!"0".equals(sqzlUser.getStatus())) {
            throw new LockedAccountException("账号已被锁定!");
        }
        return new SimpleAuthenticationInfo(user, token, getName());
    }

    /**
     * Shiro在Spring自动装配bean之前实例化
     * 相关的Bean都被初始化完成且没有被代理,使用动态获取代理对象即可解决
     */
    protected void preHandleNull() {
        if (null == userService) {
            userService = SpringContextUtils.getBean(UserService.class);
        }
    }
}

SpringContextUtils:

/**
 * SpringContextUtils工具类
 * @Author liao xiangdong
 * @Date 2021/8/10 10:50
 */
public class SpringContextUtils{
    private static ApplicationContext applicationContext;

    public static void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
    }

    public static  T getBean(Class requiredType) {
        return applicationContext.getBean(requiredType);
    }
}

MainApp:

 public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MainApp.class);
        ConfigurableApplicationContext ctx=application.run(args);
        SpringContextUtils.setApplicationContext(ctx);
    }

6、ShiroConfig

/**
 * Shiro配置类
 *
 * @Author liao xiangdong
 * @Date 2021/8/4 14:25
 */
@Configuration
public class ShiroConfig {

    @Bean
    public DefaultWebSecurityManager getManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //使用自定义realm
        manager.setRealm(new UserRealm());
        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //添加自己的过滤器并且取名为jwt
        Map filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        //自定义url规则
        Map filterRuleMap = new HashMap<>();
        filterRuleMap.put("/api/login/**", "anon");
        filterRuleMap.put("/api/register/**", "anon");
        //开放API文档接口
        filterRuleMap.put("/swagger-ui.html", "anon");
        filterRuleMap.put("/swagger-resources/**", "anon");
        filterRuleMap.put("/api/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 添加注解支持
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

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

7、解决JWT的过期与续期问题

JWT中可以指定过期时间,当前时间大于JWT中的过期时间时,JWT失效。如果指定过期时间,会导致无法续期,因为采用的方式为登陆成功时将用户名与生成的token作为键值对存入redis中,设置过期时间半小时,每次请求时刷新过期时间。从而控制JWT的过期与续期。

8、单元测试中模拟请求走自定义的Filter的问题

可以在单元测试的基础类中模拟一个filter,这样测试时不会调动远程接口

    /**
     * 使用此注解注入的类,表明类中的所有方法都使用自定义返回的值
     * 这样在测试的时候就不会真的去调用远程接口,而是返回一个我们预设的值,默认返回null
     */
    @MockBean
    private JWTFilter jwtFilter;

    @BeforeEach
    public void setupMockMvc(){
        //初始化MockMvc对象
        mvc = MockMvcBuilders.webAppContextSetup(context).addFilters(jwtFilter).build(); 
    }

你可能感兴趣的:(SpringBoot+Shiro+JWT)