学习之路 ── springboot+jwt+shiro 认证授权

   日期:2020-08-30     浏览:119    评论:0    
核心提示:import org.apache.shiro.authc.AuthenticationToken;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: *。

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实现。


项目目录


为了测试认证和授权,创建了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;

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

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

    
    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;


public class JwtFilter extends BasicHttpAuthenticationFilter {
    
    
    @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;
    }

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

    
    

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


public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserRepository userRepository;

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

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

    
    @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);

    
    @Bean
    public MyRealm myAuthRealm() {
        MyRealm myRealm = new MyRealm();
        return myRealm;
    }
    
    @Bean
    public SecurityManager securityManager() {
        // 将自定义 Realm 加进来
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());
        logger.info("====securityManager注册完成====");
        securityManager.setRealm(myAuthRealm());
        return securityManager;
    }

    
    @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***", "perms[\"user:create\"]");
        filterRuleMap.put("
@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

realm认证授权: 除了login请求不经过JwtFilter,其他请求都要经过JwtFilter,判断token是否存在,然后通过getSubject().login() 委托realm进行认证授予权限。
下图我使用的是teacher角色访问/teacher开头的url,需要的是user.*权限,但realm授予它的权限是student.*所以是这个unauthorized这个页面

token时间过期报错: token过期时间设置60秒,时间一到token失效

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

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服