SrpingBoot-Shrio 整合 JWT (7)-不够完善

42 篇文章 1 订阅
7 篇文章 0 订阅

SrpingBoot-Shrio 整合 JWT (2019.12.23)

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

一个JWT实际上就是一个字符串,它由三部分组成: 头部、载荷与签名。

1. 引入JWT依赖

<?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>

  <parent>
    <groupId>spring-boot-demo-shiro-base</groupId>
    <artifactId>spring-boot-demo-shiro-base</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <artifactId>spring-boot-demo-shiro6</artifactId>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <mybatis.version>1.3.2</mybatis.version>
    <mysql.version>8.0.12</mysql.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>${mybatis.version}</version>
    </dependency>
    <!--使用阿里巴巴的德鲁伊作为数据源-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
  <!--mysql数据库-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- thymeleaf -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--Shiro 依赖-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring-boot-starter</artifactId>
      <version>1.4.0-RC2</version>
    </dependency>
    <!--jjwt 依赖-->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.7.0</version>
    </dependency>
    <!-- shiro-redis -->
    <dependency>
      <groupId>org.crazycake</groupId>
      <artifactId>shiro-redis</artifactId>
      <version>2.4.2.1-RELEASE</version>
      <exclusions>
        <exclusion>
          <artifactId>shiro-core</artifactId>
          <groupId>org.apache.shiro</groupId>
        </exclusion>
      </exclusions>
    </dependency>
	<!--html页面使用shiro标签依赖-->
    <dependency>
      <groupId>com.github.theborakompanioni</groupId>
      <artifactId>thymeleaf-extras-shiro</artifactId>
      <version>2.0.0</version>
    </dependency>
    <!-- 对象池,使用redis时必须引入 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>
</project>

2.application.yml 加上jwt配置信息

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    #数据源其他配置
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
  redis:
    host: localhost
    # 连接超时时间(记得添加单位,Duration)
    timeout: 10000ms
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: -1ms
        # 连接池中的最大空闲连接 默认 8
        max-idle: 8
        # 连接池中的最小空闲连接 默认 0
        min-idle: 0
#jwt 配置
jwt:
  config:
    key: zhihao  #加密密匙
    # token有效期,单位秒
    jwtTimeOut: 3600
    # 后端免认证接口 url
    anonUrl: /login,
mybatis:
  mapper-locations: mapper/*.xml
  type-aliases-package: com.zhihao.entity

3.创建jwtUtil.java工具类,进行签发和解析token

@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtil {

    @Value("${jwt.config.key}")
    private String key;

    private long jwtTimeout;//一个小时

    private String anonUrl;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getJwtTimeout() {
        return jwtTimeout;
    }

    public void setJwtTimeout(long jwtTimeout) {
        this.jwtTimeout = jwtTimeout;
    }

    public String getAnonUrl() {
        return anonUrl;
    }

    public void setAnonUrl(String anonUrl) {
        this.anonUrl = anonUrl;
    }

    /**
     * 生成JWT
     *
     * @param id      用户id
     * @param subject 用户名
     * @return java.lang.String
     */
    public String createJWT(String id, String subject) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(id) //id
                .setSubject(subject) //主题
                .setIssuedAt(now) //签发时间
                .signWith(SignatureAlgorithm.HS256, key); //加密
        //超时大于0 设置token超时
        if (jwtTimeout > 0) {
            //转换成超时毫秒
            long timeout = nowMillis + (jwtTimeout * 1000);
            builder.setExpiration(new Date(timeout));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     *
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr){
        return Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();

    }
}

4.自定义自己的过滤器JWTFilter

我们使用的是 shiro 默认的权限拦截 Filter,而因为JWT的整合,我们需要自定义自己的过滤器 JWTFilter,JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原方法进行了重写。

  1. 检验请求头是否带有 token ((HttpServletRequest) request).getHeader("Token") != null
  2. 如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明当前状态为游客状态(或者其他一些不需要进行认证的接口)
public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    private static final String TOKEN = "Token";

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    private Map errorMap;


    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        JwtUtil JwtUtil = context.getBean(JwtUtil.class);
        //判断是否是登录请求
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String[] split = StringUtils.split(JwtUtil.getAnonUrl(), ",");
        for (String url : split) {
            //如果是后端免认证接口 直接放行
            if (pathMatcher.match(url,httpServletRequest.getRequestURI())){
                return true;
            }
        }
        //进入executeLogin方法判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            return executeLogin(request, response);
        }else {
            this.tokenError(response,"token为空");
        }
        return false;
    }

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

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

    /**
     * token问题响应
     *
     * @param response
     * @param msg
     * @return void
     * @author: zhihao
     * @date: 2019/12/24
     * {@link #}
     */
    private void tokenError(ServletResponse response,String msg) throws IOException {
        errorMap = new LinkedHashMap();
        errorMap.put("code", "error");
        errorMap.put("msg", msg);
        //响应token为空
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.resetBuffer(); //清空第一次流响应的内容
        //转成json格式
        ObjectMapper object = new ObjectMapper();
        String asString = object.writeValueAsString(errorMap);
        response.getWriter().println(asString);
    }

    /**
     * 对跨域提供支持
     */
    @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);
    }
}

5, JWTToken.java

@Data
public class JWTToken implements AuthenticationToken {

    private String token;

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

    public JWTToken() {
    }

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

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

}

6. 配置ShiroConfig

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 在 Shiro过滤器链上加入 JWTFilter
        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
		//所有url都必须认证通过jwt过滤器才可以访问
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        // 配置SecurityManager,并注入shiroRealm
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        //设置缓存管理器 省略..
        return securityManager;
    }

    @Bean
    public ShiroRealm shiroRealm() {
        // 配置Realm,需自己实现
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }
    /**
     * 开启shiro认证注解
     *
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

7. 配置自己实现的ShiroRealm

public class ShiroRealm extends AuthorizingRealm {

    /**
     *  支持自定义认证令牌
     *
     * @param token
     * @return boolean
     * @author: zhihao
     * @date: 2019/12/24
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 授权模块>>>>获取用户角色和权限
     *
     * @param principal
     * @return org.apache.shiro.authz.AuthorizationInfo
     * @author: zhihao
     * @date: 2019/12/13
     * {@link #}
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
       	User user = (User) SecurityUtils.getSubject().getPrincipal();
        String username = user.getUsername();
        //创建授权对象进行封装角色和权限信息进去进行返回 注意不是SimpleAuthenticationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获取用户角色集
        List<Role> roleList = roleService.findRoleByUserName(username);
        Set<String> roleSet = new HashSet<>();
        for (Role role : roleList) {
            roleSet.add(role.getMemo());
        }
        System.out.println("用户拥有的角色>>>"+roleSet);
        //添加角色进角色授权
        info.setRoles(roleSet);
        List<Permission> permissionList = permissionService.findPermissionByUserName(username);
        Set<String> permissionSet = new HashSet<>();
        for (Permission permission : permissionList) {
            permissionSet.add(permission.getName());
        }
        System.out.println("用户拥有的权限>>>"+permissionSet);
        //添加权限进权限授权
        info.setStringPermissions(permissionSet);
        return info;
    }

    /**
     * 用户认证
     *
     * @param authenticationToken 身份认证 token
     * @return AuthenticationInfo 身份认证信息
     * @throws AuthenticationException 认证相关异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {

        // 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的
        String token = (String) authenticationToken.getCredentials();
        String username = null;
        try {
            username = jwtUtil.parseJWT(token).getSubject();
        } catch (Exception e) {
            //抛出token认证失败
            throw new AuthenticationException("token认证失败");
        }
        // 通过用户名到数据库查询用户信息
        User user = userService.findUserByName(username);
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        if (user.getStatus().equals("0")) {
            throw new LockedAccountException("账号已被锁定,请联系管理员!");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, token, getName());
        return info;
    }
}

7. 配置权限不足异常处理器

/**
 * @Author: zhihao
 * @Date: 2019/12/24 12:57
 * @Description: 登录相关异常处理
 * @Versions 1.0
 **/
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class loginExceptionHandler {
    private Logger log = LoggerFactory.getLogger(this.getClass());

    private Map exceptionMap;
    /**
     * 拦截权限不足异常,并响应
     *
     * @param e
     * @return java.util.Map
     * @author: zhihao
     * @date: 2019/12/24
     */
    @ExceptionHandler(value = UnauthorizedException.class)
    @ResponseBody
    public Map unauthorizedException(UnauthorizedException e){
        exceptionMap = new LinkedHashMap();
        exceptionMap.put("msg", e.getMessage());
        log.error(e.getMessage());
        return exceptionMap;
    }
}

8.登录接口,校验成功签发token

 /**
     * 登录免认证  登录成功签发token
     *
     * @param username 用户名
     * @param password 密码
     * @return java.util.Map 简陋的结果包装
     * @author: zhihao
     * @date: 2019/12/12
     */
    @PostMapping("/login")
    public Map login(@RequestParam("username") String username,@RequestParam("password") String password){
        resultMap = new LinkedHashMap<>();
        // 密码加密
        String md5 = new SimpleHash("MD5", password, username, 1024).toString();
        User user = userService.findUserByName(username);
        if(user != null && md5.equals(user.getPassword())){
            resultMap.put("code", "success");
            resultMap.put("token", jwtUtil.createJWT(user.getId(), user.getUsername()));
            return resultMap;
        }
        resultMap.put("code","error");
        resultMap.put("msg","用户不存在或者密码错误");
        return resultMap;
    }

9. 进行测试

项目代码:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懵懵懂懂程序员

如果节省了你的时间, 请鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值