最近在搭建阿里巴巴的微服务框架,这次是引入jwt实现鉴权,主要包括以下功能

(1)登录。接收用户名,密码,校验密码是否正确,若正确则返回token(jwt生成),若错误返回提示信息。

(2)请求网关时校验token。

(3)登出。接收token,将指定token置为失效的状态。

(4)续签。对前端服务部署服务器发过来的请求对过期的token直接返回新的token,并提示更换token。

功能实现涉及两个服务,网关服务,鉴权服务。

 

一、在鉴权服务中

pom关键配置:

    <properties>  
        <jjwt.version>0.11.5</jjwt.version>
    </properties>

    <dependencies>
        <!--springboot基本场景启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.0</version>
            <exclusions></exclusions>
        </dependency>
        <!--引入JWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--处理token过期时间-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.9.6</version>
        </dependency>
    </dependencies>

 

配置文件关键配置:

auth:
  expireMinutes: 20 #过期分钟数
  key: 0123456789_0123456789_0123456789 #token生成

#省略网关,redis等其他常规设置

 

 工具类(关键代码)

package com.example.auth.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.auth.constant.JWTConstants;
import com.example.auth.model.AuthUser;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * jwt工具类
 */
@Component
public class JwtUtils {

    @Value("${auth.expireMinutes:30}")
    private int expireMinutes;

    /**
     * 生成密钥
     *
     * @return 生成密钥
     */
    Algorithm getAlgorithm() {
        return Algorithm.HMAC256(JWTConstants.JWT_KEY);
    }

    /**
     * 生成token
     *
     * @param userId   用户id
     * @param userName 用户名
     * @param userRole 用户的角色
     * @return token jwtToken
     */
    public String generateToken(String userId, String userName, String userRole) {
        return JWT.create()
                .withClaim(JWTConstants.JWT_KEY_USER_NAME, userName)
                .withClaim(JWTConstants.JWT_KEY_ROLE, userRole)
                .withClaim(JWTConstants.JWT_KEY_ID, userId)
                .withExpiresAt(DateTime.now().plusSeconds(expireMinutes).toDate())
                .sign(getAlgorithm());
    }

    /**
     * 根据用户信息生成用户
     *
     * @param user 用户信息
     * @return token jwtT   oken
     */
    public String generateToken(AuthUser user) {
        return generateToken(user.getId(), user.getUsername(), user.getRole());
    }

    /**
     * 解码token
     *
     * @param token jwtToken
     * @return 用户信息
     */
    public AuthUser decode(String token) {
        AuthUser authUser = new AuthUser();
        DecodedJWT decodedJWT = JWT.require(getAlgorithm()).build().verify(token);
        Map<String, Claim> jwt = decodedJWT.getClaims();
        String userName = jwt.get(JWTConstants.JWT_KEY_USER_NAME).asString();
        String userId = jwt.get(JWTConstants.JWT_KEY_ID).asString();
        authUser.setId(userId);
        authUser.setUsername(userName);
        return authUser;
    }

}

 

登录接口

package com.example.auth.controller;

import com.alibaba.nacos.common.model.RestResult;
import com.example.auth.model.AuthUser;
import com.example.auth.model.LoginReturn;
import com.example.auth.utils.JwtUtils;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("auth")
@RefreshScope
public class AuthController {
    @Resource
    JwtUtils jwtUtils;

    @PostMapping("/login")
    public RestResult<String> login(@RequestBody AuthUser user) {

        //模拟数据库查询的用户
        AuthUser tagetUser = new AuthUser();
        tagetUser.setPassword("123456");

        //密码校验(demo密码未加密)
        if (!tagetUser.getPassword().equals(user.getPassword())) {
            return new RestResult<>(-1, "用户名与密码不正确");
        }

        String token = jwtUtils.generateToken(user);
        return new RestResult(1, "认证成功", new LoginReturn(user,token));
    }

}

 

 其他实体类

package com.example.auth.model;

import lombok.*;


/**
 * @author songyan
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class AuthUser {

    /**
     * 主键
     */
    private String id;

    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 角色
     */
    private String role;
}
AuthUser
LoginReturn
package com.example.auth.constant;

public class JWTConstants {

    public static final String JWT_REQUEST_HEADER_KEY = "Authorization";
    public static final String JWT_KEY = "0123456789_0123456789_0123456789";

    public static final String JWT_KEY_ID = "user_id";

    public static final String JWT_KEY_USER_NAME = "user_name";

    public static final String JWT_KEY_ROLE = "user_role";
    public static final String JWT_REQUEST_KEY_ID = JWT_KEY_ID;

    public static final String JWT_REQUEST_KEY_USER_NAME = JWT_KEY_USER_NAME;

    public static final String JWT_REQUEST_KEY_ROLE = JWT_KEY_ROLE;
}
JWTConstants
package com.example.auth.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
RedisConfiguration
package com.example.auth.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }

        }

    }

    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return*/
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

}
RedisUtil

二、网关服务

依赖

        <!--依赖权限模块-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>auth</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 

JwtTokenFilter  全局过滤器
package com.luoxun.gateway.filter;


import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.model.RestResult;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.auth.constant.JWTConstants;
import com.example.auth.model.AuthUser;
import com.example.auth.utils.JwtUtils;
import com.example.auth.utils.RedisUtil;
import com.luoxun.gateway.constant.AuthConstant;
import com.luoxun.gateway.constant.AuthReturnMessage;
import com.luoxun.gateway.constant.BEANOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;

/**
 * JwtToken 过滤器
 */
@Order(BEANOrder.JWT_TOKEN_FILTER)
@Component
@Slf4j
public class JwtTokenFilter implements GlobalFilter {

    @Value("${auth.tt}")
    private String skipAuthUrls;
    @Value("${auth.overduceTime}")
    private int overdueTime;

    @Value("${auth.webIp}")
    private String webIp;
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private RedisUtil redisUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳过不需要验证的路径
        if (skipAuthUrls != null && url.equals(skipAuthUrls)) {
            //继续路由
            return chain.filter(exchange);
        }

        //获取token
        String token = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_REQUEST_HEADER_KEY);
        ServerHttpResponse resp = exchange.getResponse();

        if (StringUtils.isBlank(token)) {
            return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_NULL);
        } else {
            try {

                //校验token并解析token
                AuthUser loginUser = jwtUtils.   decode(token);

                //登出
                if (url.equals(AuthConstant.URL_LOGOUT)) {
                    redisUtil.set(AuthConstant.PREFIX_TOKEN_LAPSED + loginUser.getUsername(), token, overdueTime);
                    return authSuccess(resp, AuthReturnMessage.AUTH_SUCCESS_LOGOUT);
                }

                //判断token是否已弃用
                String sig = redisUtil.get(AuthConstant.PREFIX_TOKEN_LAPSED + loginUser.getUsername()) + "";
                if (token.equals(sig)) {
                    return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_LAPSED);
                }

                //继续路由
                return chain.filter(exchange);
            } catch (TokenExpiredException tokenExpiredException) {
                //处理过期的token
                return expiredToken(tokenExpiredException, exchange, url, token);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_FAIL);
            }
        }
    }

    /**
     * token过期的处理
     *
     * @param tokenExpiredException
     * @param exchange
     * @param url
     * @param token
     * @return
     */
    private Mono<Void> expiredToken(TokenExpiredException tokenExpiredException, ServerWebExchange exchange, String url, String token) {
        log.error(tokenExpiredException.getMessage(), tokenExpiredException);
        String userName = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_REQUEST_KEY_USER_NAME);
        String userId = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_REQUEST_KEY_ID);
        String userRole = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_REQUEST_KEY_ROLE);
        ServerHttpRequest request = exchange.getRequest();
        String ip = request.getURI().getHost();
        ServerHttpResponse resp = exchange.getResponse();
        if (webIp.equals(ip)) {
            //登出
            if (url.equals(AuthConstant.URL_LOGOUT)) {
                return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_EXPIRED);
            }
            //判断token是否已弃用
            String sig = redisUtil.get(AuthConstant.PREFIX_TOKEN_LAPSED + userName) + "";
            if (token.equals(sig)) {
                return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_EXPIRED);
            }
            //续签
            String newToken = jwtUtils.generateToken(userId, userName, userRole);
            return authSuccess(resp, new RestResult(HttpStatus.OK.value(), AuthReturnMessage.AUTH_SUCCESS_TIP_REFRESH, newToken));
        }
        return authError(resp, AuthReturnMessage.AUTH_EXCEPTION_EXPIRED);
    }

    /**
     * 认证错误输出
     *
     * @param resp 响应对象
     * @param mess 错误信息
     * @return
     */
    private Mono<Void> authError(ServerHttpResponse resp, String mess) {
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        DataBuffer buffer = resp.bufferFactory().wrap(mess.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    private Mono<Void> authSuccess(ServerHttpResponse resp, RestResult mess) {
        resp.setStatusCode(HttpStatus.OK);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        DataBuffer buffer = resp.bufferFactory().wrap(JSONObject.toJSONString(mess).getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    private Mono<Void> authSuccess(ServerHttpResponse resp, String mess) {
        resp.setStatusCode(HttpStatus.OK);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        DataBuffer buffer = resp.bufferFactory().wrap(mess.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

}

 

其他

package com.luoxun.gateway.configuration;

import com.example.auth.utils.JwtUtils;
import com.example.auth.utils.RedisUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    /**
     * 注入jwt工具类
     *
     * @return jwt工具类
     */
    @Bean
    public JwtUtils jwtUtils() {
        return new JwtUtils();
    }

    @Bean
    public RedisUtil redisUtil() {
        return new RedisUtil();
    }
}
CommonConfiguration
package com.luoxun.gateway.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
RedisConfiguration
package com.luoxun.gateway.constant;

public class AuthConstant {
    public final static String PREFIX_TOKEN_LAPSED = "token_lapsed_";

    public final static String URL_LOGOUT = "/auth/loginOut";
}
AuthConstant
package com.luoxun.gateway.constant;

public class AuthReturnMessage {
    public final static String AUTH_EXCEPTION_FAIL = "认证失败";
    public final static String AUTH_EXCEPTION_LAPSED = "认证失效";
    public final static String AUTH_EXCEPTION_EXPIRED = "认证过期";

    public final static String AUTH_EXCEPTION_NULL = "认证信息不可为空";
    public final static String AUTH_SUCCESS_LOGOUT = "已登出";
    public final static String AUTH_SUCCESS_TIP_REFRESH = "请更换新token";
}
AuthReturnMessage
package com.luoxun.gateway.constant;

public class BEANOrder {
    public static final int JWT_TOKEN_FILTER = -100;
}
BEANOrder

 

posted on 2022-12-02 09:14  song.yan  阅读(936)  评论(0编辑  收藏  举报