Spring Boot 集成 JWT 实现单点登录授权

认识:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

使用步骤如下:

1. 添加Gradle依赖:

dependencies {
    implementation 'com.auth0:java-jwt:3.3.0'
    implementation('org.springframework.boot:spring-boot-starter-aop')
}

2. 登录检验时,使用JWT生成Token令牌(我这里登录用户名是email)。

    /**
     * 登录检验方法。
     * @param user
     * @return
     */
    public String login(User user) {
        // 登录检验逻辑 TODO

        //登录检验成功,生成token令牌
        String token = tokenService.generateToken(userRepository.findUserByMail(user.getMail()));

        //自定义返回值
        return null;
    }

一、生成和校验Token。

1)生成和校验token令牌的服务类。

/**
 * 使用JWT作为Token实现。
 *
 * @author LEEMER
 * Create Date: 2019-05-20
 */
@Service
public class TokenService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TokenService.class);

    private TokenConfigBean tokenConfigBean;

    /**
     * Token加密算法。
     */
    private Algorithm algorithm;

    /**
     * Token认证对象。
     */
    private JWTVerifier verifier;

    private UserRepository userRepository;

    public TokenService(TokenConfigBean tokenConfigBean,
                        Algorithm algorithm,
                        JWTVerifier verifier,
                        UserRepository userRepository) {
        this.tokenConfigBean = tokenConfigBean;
        this.algorithm = algorithm;
        this.verifier = verifier;
        this.userRepository = userRepository;
    }

    public String generateToken(User user) {
        JWTCreator.Builder builder = JWT.create().withClaim("mail", user.getMail());

        builder.withClaim("expiredTime", System.currentTimeMillis()
                + tokenConfigBean.getExpiredTime() * 60 * 1000);

        return builder.sign(algorithm);
    }

    public boolean isValid(String token) {
        DecodedJWT jwt;
        try {
            jwt = verifier.verify(token);
        } catch (JWTVerificationException exception){
            LOGGER.debug("该Token解码失败:" + token);
            return false;
        }
        long expiredTime = jwt.getClaim("expiredTime").asLong();
        if (expiredTime < System.currentTimeMillis()) {
            LOGGER.debug("该Token已过期:" + expiredTime);
            return false;
        }
        return true;
    }

    public String resetExpiredTime(String token) {
        DecodedJWT jwt;
        try {
            jwt = verifier.verify(token);
        } catch (JWTVerificationException exception){
            return "";
        }
        User user = userRepository.findUserByMail(jwt.getClaim("mail").asString());
        return generateToken(user);
    }

    public String getValueByMail(String token, String key) {
        var trueToken = token.startsWith("Bearer ") ? token.substring(7) : token;
        DecodedJWT jwt;
        try {
            jwt = verifier.verify(trueToken);
        } catch (JWTVerificationException exception){
            LOGGER.debug("目标Token解析失败:" + trueToken);
            return null;
        }
        return jwt.getClaim(key).asString();
    }

    public String getValueByMail(String token, int mail) {
        return getValueByMail(token, String.valueOf(mail));
    }

    public  List getListValueByMail(String token, String mail, Class cls) {
        DecodedJWT jwt;
        try {
            jwt = verifier.verify(token);
        } catch (JWTVerificationException exception){
            return null;
        }
        return jwt.getClaim(mail).asList(cls);
    }

    public  List getListValueByMail(String token, int mail, Class cls) {
        return getListValueByMail(token, String.valueOf(mail), cls);
    }

    public User getUser(String token) {
        var mail = getValueByMail(token, "mail");
        if (StringUtils.isBlank(mail)) {
            LOGGER.error("[getUser] 从该Token中无法提取有效邮箱:{}", token);
            return null;
        }
        return userRepository.findUserByMail(mail);
    }
}

2)Token配置Bean:设置token过期时间、混淆。

/**
 * @author LEEMER
 * Create Date: 2019-05-20
 */
@Configuration
public class TokenConfigBean {

    /**
     * Token过期时间(单位:分钟)。
     */
    private int expiredTime = 30;

    /**
     * 混淆。
     */
    private String secret = "c8e3n23ia0wgn458yqwafn934uf";

    public int getExpiredTime() {
        return expiredTime;
    }

    public void setExpiredTime(int expiredTime) {
        this.expiredTime = expiredTime;
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }
}

3)Token认证对象和加密算法配置类。

/**
 * @author LEEMER
 * Create Date: 2019-05-20
 */
@Component
public class BeanConfig {

    private TokenConfigBean tokenConfigBean;

    public BeanConfig(TokenConfigBean tokenConfigBean) {
        this.tokenConfigBean = tokenConfigBean;
    }

    /**
     * Token认证对象。
     */
    @Autowired
    @Bean
    public JWTVerifier getJwtVerifier(Algorithm algorithm) {
        return JWT.require(algorithm).build();
    }

    /**
     * Token加密算法。
     */
    @Bean
    public Algorithm getAlgorithm() {
        try {
            return Algorithm.HMAC256(tokenConfigBean.getSecret());
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("生成Token加法算法失败!", e);
            throw new RuntimeException(e);
        }
    }

}

4)自定义控制器请求token认证注解。

/**
 * 使用在传统控制器的方法上,进行登录判断,如果没有登录则返回登录界面。
 *
 * @author LEEMER
 * Create Date: 2019-05-20
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ControllerAuthCheck {
}
/**
 * @author LEEMER
 * Create Date: 2019-05-20
 */
@Aspect
@Component
public class ControllerAuthCheckAspect {

    private static final Logger LOGGER = LoggerFactory
            .getLogger(ControllerAuthCheckAspect.class);

    private UrlConfigBean urlConfigBean;

    /**
     * Token服务。
     */
    private TokenService tokenService;

    @Autowired
    public ControllerAuthCheckAspect(TokenService tokenService,
                                     UrlConfigBean urlConfigBean) {
        this.tokenService = tokenService;
        this.urlConfigBean = urlConfigBean;
    }

    /**
     * 定义切入点。
     *
     * 所有包含AuthCheck注解的方法均会被拦截。
     */
    @Pointcut("@annotation(cn.blackbox.annotation.ControllerAuthCheck)")
    private void checkAuth() {}

    /**
     * 环绕增强。
     *
     * 在调用相关系统模块时,进行权限检查,没有相关权限则不进行目标方法的调用。
     *
     */
    @Around(value = "checkAuth() && @annotation(controllerAuthCheck) && args(token, ..)",
            argNames = "joinPoint, controllerAuthCheck, token")
    private Object checkAuth(ProceedingJoinPoint joinPoint,
                             ControllerAuthCheck controllerAuthCheck,
                             String token) {
        if (!token.startsWith("Bearer ")) {
            return "redirect:" + urlConfigBean.getLogin();
        } else {
            token = token.substring(7);
        }

        if (!tokenService.isValid(token)) {
            return "redirect:" + urlConfigBean.getLogin();
        }

        try {
            return  joinPoint.proceed();
        } catch (Throwable throwable) {
            LOGGER.error("控制器权限控制切面调用目标方法失败!", throwable);
            throw new RuntimeException(throwable);
        }
    }
}

 

二、使用Token实现相关模块登录认证。

使用场景:请求后台数据的时候我们可以通过token值判断用户是否登录。

1)在JS里面我们可以localStorage.getItem("token")获取token值。如下:

function getLoginDetails() {
	$.ajax({
		url:"/api/v1/stage/land_details",
		method:"GET",
		dataType:"JSON",
		headers:{
			token:localStorage.getItem("token")
		},
		success : function (result) {

		}
	})
}

2)后台校验token值,通过我们自定义@ControllerAuthCheck注解实现token校验功能。

@RequestMapping("/land_details")
@ControllerAuthCheck // 检验token的注解,必须要获取token参数才能有效校验
public String toLandDetailsView(@RequestParam(value = "token", required = false, defaultValue = "") String token){
    return "/land_details";
}

ojbk

你可能感兴趣的:(Java)