欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

springboot+jwt+shiro 登录认证授权

程序员文章站 2022-04-15 17:50:09
import org.apache.shiro.authc.AuthenticationToken;/** * 封装了需要传递的信息 * 类似UsernamePasswordToken */public class JwtToken implements AuthenticationToken { private String jwtoken; public JwtToken(String jwtoken) { this.jwtoken = jwtoken;...

JWT

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

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
springboot+jwt+shiro 登录认证授权

JWT的构成

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

header

jwt的头部承载两部分信息:

声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256

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

对应base64UrlEncode编码为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
说明:该字段为json格式。alg字段指定了生成signature的算法,默认值为 HS256,typ默认值为JWT
通过header中声明的加密方式进行加密(该加密是可以对称解密的),构成了第一部分.

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

标准中注册的声明
公共的声明
私有的声明
标准中注册的声明 (建议但不强制使用) :

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

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

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
} 

对应base64UrlEncode编码为:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

说明:该字段为json格式,表明用户身份的数据,可以自己自定义字段,很灵活。sub 面向的用户,name 姓名 ,iat 签发时间。例如可自定义示例如下:
通过header中声明的加密方式加密(该加密是可以对称解密的),构成了第一部分.

signature

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

header (base64后的)
payload (base64后的)
secret

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  123456
) 

对应的签名为:keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU 

说明:对header和payload进行base64UrlEncode编码后进行拼接。通过key(这里是123456)进行HS256算法签名。
最终得到的JWT的Token为(header.payload.signature):
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式加上secret组合加密,然后就构成了jwt的第三部分。

JWT与Session的区别

相同点是,它们都是存储用户信息; Session 在服务器端 JWT 在客户端

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

安全相关

secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了

shiro (java安全框架)

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

三个核心组件:

Subject: 即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。

SecurityManager: 它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。


项目目录

springboot+jwt+shiro 登录认证授权springboot+jwt+shiro 登录认证授权

为了测试认证和授权,创建了3个实体类,分别对应用户、角色和权限

项目流程:
如果是login请求,不经过JwtFilter,直接通过JwtUtls生成token;如果是其他请求,通过JwtFilter判断token是否为空,委托Myrealm对token进行认证授权,认证失败就报错,成功就授予用户角色权限。

maven依赖

<!--引入JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--引入shiro依赖--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> 

JwtToken封装类

import org.apache.shiro.authc.AuthenticationToken; /**
 * 封装了需要传递的信息
 * 类似UsernamePasswordToken
 */ public class JwtToken implements AuthenticationToken { private String jwtoken; public JwtToken(String jwtoken) { this.jwtoken = jwtoken; } //获取身份 @Override public Object getPrincipal() { return jwtoken; } //获取凭证 @Override public Object getCredentials() { return jwtoken; } } 

**JwtUtils工具类:**生成、校验token

import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTCreationException; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; public class JwtUtils { private static final long EXPIRE_TIME = 60 * 1000; private static final String SECRET = "huangwc"; /**
     * @Description: 校验token
     * @Date: 2020/8/26 11:24
     **/ public static boolean verify(String token, String username){ try{ //获取加密算法对象(密钥) Algorithm algorithm = Algorithm.HMAC256(SECRET); //获取JWT 验证对象 JWTVerifier verifier = JWT.require(algorithm) .withClaim("username",username) .build(); DecodedJWT jwt = verifier.verify(token); return true; }catch (Exception e){ return false; } } /**
     * @Description: 创建token
     * @Date: 2020/8/26 11:25
     **/ public static String sign(String username){ try{ Date data = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(SECRET); return JWT.create() .withClaim("username",username) .withExpiresAt(data) .sign(algorithm); }catch (Exception e){ return null; } } /**
     * @Description: 通过token,获取用户名
     * @Date: 2020/8/26 11:25
     **/ public static String getUsername(String token){ if (token == null || "".equals(token)){ return null; } try{ DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); }catch (JWTCreationException e){ return null; } } } 

JwtFilter

import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /**
 * 自定义一个Filter,用来拦截所有的请求判断是否携带Token
 * */ public class JwtFilter extends BasicHttpAuthenticationFilter { /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */ /**
     * @Description: isAccessAllowed()判断是否携带了有效的JwtToken
     * @Date: 2020/8/26 13:39
     **/ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { try { return executeLogin(request, response); } catch (Exception e) { throw new AuthorizationException("权限不足", e); } } return true; } /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; //判断是否是登录请求 //与前端约定,要求前端将jwtToken放在请求的Header部分 //所以以后发起请求的时候就需要在Header中放一个token,值就是对应的Token String authorization = req.getHeader("token"); return authorization != null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; Map<String, String> map = new HashMap<>(2); String header = req.getHeader("token"); JwtToken token = new JwtToken(header); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 try { getSubject(request, response).login(token); // 如果没有抛出异常则代表登入成功,返回true } catch (Exception e) { e.printStackTrace(); //调用下面的方法向客户端返回错误信息 return false; } return true; } /**
     * 此方法相当于isLoginAttempt()和executeLogin()
     * @Description: onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
     * @Date: 2020/8/26 13:40
     **/ /*@Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        JwtToken jwtToken = new JwtToken(jwt);
        try {
            // 委托 realm 进行登录认证
            //所以这个地方最终还是调用JwtRealm进行的认证
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        //执行方法中没有抛出异常就表示登录成功
        return true;
    }*/ /**
     * 对跨域提供支持
     */ @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); } } 

MyRealm

import com.example.jwtshirodemo.dao.UserRepository; import com.example.jwtshirodemo.entity.Role; import com.example.jwtshirodemo.entity.User; import com.example.jwtshirodemo.jwt.JwtToken; import com.example.jwtshirodemo.jwt.JwtUtils; 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.HashSet; import java.util.Set; /**
 * doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
 * doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
 */ public class MyRealm extends AuthorizingRealm { @Autowired private UserRepository userRepository; /**
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     * 不负责验证其他的token(UsernamePasswordToken)
     * */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /**
     * @Description: 授权
     * @Date: 2020/8/26 14:09
     **/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取token String token = principalCollection.getPrimaryPrincipal().toString(); System.out.println("token:" + token); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 给该用户设置角色,角色信息存在 t_role 表中取 String username = JwtUtils.getUsername(token); User user = userRepository.getByUsername(username); Set<String> roles = new HashSet<>(); for (Role role : user.getRoles()){ roles.add(role.getRolename()); } authorizationInfo.setRoles(roles); // 给该用户设置权限,权限信息存在 t_permission 表中取 authorizationInfo.setStringPermissions(userRepository.getPermissions(roles)); return authorizationInfo; } /**
     * @Description: 认证
     * @Date: 2020/8/26 14:09
     **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取token String token = authenticationToken.getPrincipal().toString(); System.out.println("token:" + token); if (token == null) { throw new NullPointerException("token 不允许为空"); } String username = JwtUtils.getUsername(token); //判断 if (!JwtUtils.verify(token,username)) { throw new UnknownAccountException(); } // 根据用户名从数据库中查询该用户,判断是否真实存在 User user = userRepository.getByUsername(username); if(user != null) { // 传入用户名和密码进行身份认证,并返回认证信息 // 这里返回的是账号密码,但是JwtToken都是jwt字符串。还需要一个该Realm(MyRealm)的类名 AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(token, token, getName()); return authcInfo; } else { return null; } } } 

ShiroConfig

import com.example.jwtshirodemo.jwt.JwtFilter; import com.example.jwtshirodemo.shiro.MyRealm; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; @Configuration public class ShiroConfig { private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class); /**
     * 注入自定义的 Realm
     * @return MyRealm
     */ @Bean public MyRealm myAuthRealm() { MyRealm myRealm = new MyRealm(); return myRealm; } /**
     * 注入安全管理器
     * @return SecurityManager
     */ @Bean public SecurityManager securityManager() { // 将自定义 Realm 加进来 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm()); logger.info("====securityManager注册完成===="); securityManager.setRealm(myAuthRealm()); return securityManager; } /**
     * 注入 Shiro 过滤器
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean
     */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { // 定义 shiroFactoryBean ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean(); // 设置自定义的 securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 设置默认登录的 URL,身份认证失败会访问该 URL shiroFilterFactoryBean.setLoginUrl("/login1"); // 设置成功之后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/success"); // 设置未授权界面,权限认证失败会访问该 URL shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); //添加jwtFilter注册到shiro的Filter中,指定除了login和logout之外的请求都先经过jwtFilter Map<String, Filter> filterMap = new HashMap<>(); //这个地方其实另外两个filter可以不设置,默认就是 filterMap.put("jwt", new JwtFilter()); //filterMap.put("anon", new AnonymousFilter()); //filterMap.put("logout", new LogoutFilter()); shiroFilterFactoryBean.setFilters(filterMap); // LinkedHashMap 是有序的,进行顺序拦截器配置 // 拦截器 Map<String, String> filterRuleMap = new LinkedHashMap<>(); filterRuleMap.put("/login", "anon"); filterRuleMap.put("/logout", "logout"); // “/student” 开头的用户需要角色认证,是“admin”才允许 filterRuleMap.put("/student*/**", "roles[admin]"); // “/teacher” 开头的用户需要权限认证,是“user:create”才允许 filterRuleMap.put("/teacher*/**", "perms[\"user:create\"]"); filterRuleMap.put("/**", "jwt"); // 以“/admin” 开头的用户需要身份认证,authc 表示要进行身份认证 // filterRuleMap.put("/admin*", "anon"); // 设置 shiroFilterFactoryBean 的 FilterChainDefinitionMap
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap); return shiroFilterFactoryBean; } } 

LoginController

import com.example.jwtshirodemo.dao.UserRepository; import com.example.jwtshirodemo.entity.User; import com.example.jwtshirodemo.jwt.JwtUtils; import org.springframework.beans.factory.annotation.Autowired; 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; /**
 * @Author: huangwc
 * @Description: 操作测试类
 * @Date: 2020/08/26 14:50:39
 * @Version: 1.0
 */ @Controller public class LoginController { @Autowired UserRepository userRepository; @RequestMapping("/login") public ResponseEntity<Map<String, String>> login(String username, String password) { Map<String, String> map = new HashMap<>(2); User user = userRepository.getByUsername(username); if (user.getUsername().equals(username) && user.getPassword().equals(password)) { String token = JwtUtils.sign(username); map.put("msg", "登录成功"); map.put("token", token); return ResponseEntity.ok(map); } map.put("msg", "用户名密码错误"); return ResponseEntity.ok(map); } /**
     * 身份认证测试接口
     */ @RequestMapping("/admin") public String admin() { return "success"; } /**
     * 角色认证测试接口
     */ @RequestMapping("/student") public String student() { return "success"; } /**
     * 权限认证测试接口
     */ @RequestMapping("/teacher") public String teacher() { return "success"; } } 

使用Postman测试接口

登录生成token: 由于在ShiroConfig设置了过滤条件,login请求不用经过JwtFilter过滤器,直接调用JwtUtils生成token
springboot+jwt+shiro 登录认证授权
realm认证授权: 除了login请求不经过JwtFilter,其他请求都要经过JwtFilter,判断token是否存在,然后通过getSubject().login() 委托realm进行认证授予权限。
下图我使用的是teacher角色访问/teacher开头的url,需要的是user.*权限,但realm授予它的权限是student.*所以是这个unauthorized这个页面
springboot+jwt+shiro 登录认证授权
token时间过期报错: token过期时间设置60秒,时间一到token失效
springboot+jwt+shiro 登录认证授权

Github项目链接:https://github.com/smallfatsheep/jwt-shiro-demo.git

本文地址:https://blog.csdn.net/weixin_42830314/article/details/108244561

相关标签: Java shiro jwt