Springboot在使用JWT单点登录的时候,尽量使用Https协议,为了实现JWT单点登录前,需要搭建含有SSL证书的互通方式,为了模拟项目,采用JDK提供的自签的方式,生成SSL自签证书,并集成到Springboot项目中,如果已经有企业SSL证书,跳过即可


JDK生成自签证书

在cmd命令下运行jdk自带的keytool工具(没有配置jdk环境变量的需要在jdk的bin目录下):

keytool -genkey -alias mypfx -keypass 111111 -storetype PKCS12 -keyalg RSA -keysize 1024 -validity 365 -keystore D:/Git/mypfx.p12 -storepass 111111

参数解释:

  • alias mypfx(证书别名) 
  • keypass 111111(别名密码) 
  • keyalg RSA(算法) 
  • keysize 1024(密钥长度) 
  • validity 365(有效期,天单位) 
  • keystore D:/Git/mypfx.p12(指定生成证书的位置和证书名称) 
  • storepass 111111(获取证书信息的密码)
  • storetype 指定密钥仓库类型

 

springboot单点登录session_单点登录

内容根据个人情况输入即可,我这里随便填写了几个

以上是我用jdk生成自签SSL证书的过程,具体还需要自己去了解SSL证书的原理


Springboot采用JWT实现单点登录

搭建springboot项目,实现https协议

    构建基础springboot项目,springboot版本:2.1.3.RELEASE

    在application.properties配置文件中加入刚才生成的自签文件

server.port=443
#证书文件路径
server.ssl.key-store=D:/Git/mypfx.p12
#别名
server.ssl.key-alias=mypfx
server.ssl.enabled=true
#密码
server.ssl.key-store-password=111111
#证书类型
server.ssl.key-store-type=PKCS12

     添加config配置文件,实现http协议自动跳转到https访问

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Https协议端口号配置
 *
 * @author AuroraLove
 * @date 2019/3/29
 */
@Configuration
public class HttpsConfiguration {

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint constraint = new SecurityConstraint();
                constraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                constraint.addCollection(collection);
                context.addConstraint(constraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(httpConnector());
        return tomcat;
    }

    private Connector httpConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        //Connector监听的http的端口号
        connector.setPort(80);
        connector.setSecure(false);
        //监听到http的端口号后转向到的https的端口号
        connector.setRedirectPort(443);
        return connector;
    }

}

运行启动项目测试:

springboot单点登录session_单点登录_02


前面做了那么多准备工作,接下来开始搭建JWT单点登录(重头戏来了)

JWT单点登录

       JWT单点登录在分布式系统,微服务架构中应用广泛,他由传统的Sesion会话管理变为了服务器自签认证,是一种无状态的验证登录,原理我就不多说了,网上都可以找到,但是必须要对JWT信息有一定的基本了解:

eyJ1aWQiOjE5NTAyLCJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJpc3MiOiJodHRwOi8vd3d3LmJsdWV0aGluay5jbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2IiwiaWF0IjoxNTUzODQ0OTY0fQ
.RdHEmhdwNrvz4dzD3fmCBbr9ye-O1R96MzzNmQX3kwI

上面这个JWT 的三个部分依次如下,信息以"."为分隔

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

这三部分中,头部是保存签名算法以及令牌类型信息的,中间负载则是存放用户自定义信息或官方定义的信息,尾部签名则是服务器端添加的签名,确保前两部分的安全

有了基本的信息了解之后,开始搭建项目:

我的项目结构图:

springboot单点登录session_HTTPS_03

增加pom依赖

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

自定义token注解

/**
 * 自定义token注解
 *
 * @author AuroraLove
 * @date 2019/3/29
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
    boolean required() default true;
}
用户登录验证成功时,生成token秘钥(为了便于验证,连接mysql数据库,建立一张简单user表)

springboot单点登录session_HTTPS_04

/**
 * @author AuroraLove
 * @date 2019/3/29
 */
@Service
public class UserService {

    @Autowired
    private static Logger logger = LoggerFactory.getLogger(UserService.class);
    @Autowired
    private UserMapper userMapper;

    public UserModel login(UserFilter ufilter){
        Optional<UserEntity> user = userMapper.findByPhone(ufilter.getPhone());
        if (user.isPresent()){
            if (user.get().getPassword().equals(ufilter.getPassword())){
                //验证用户登录成功后,生成token令牌
                String token = getToken(user.get());
                logger.info("token:" + token);
                UserModel userModel = new UserModel(user.get(),token);
                return userModel;
            }
        }
        return null;
    }

    /**
     * 生成token
     * @param userEntity
     * @return
     */
    private String getToken(UserEntity userEntity) {
        //设置算法为HS256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        Date now = new Date(System.currentTimeMillis());
        JwtBuilder builder = Jwts.builder()
                //设置header
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //设置iat
                .setIssuedAt(now)
                //设置payload的键值对
                .claim("name", userEntity.getName())
                .claim("id", userEntity.getId())
                .claim("phone", userEntity.getPhone())
                .setIssuer("AuroraLove")
                //签名,需要算法和key
                .signWith(signatureAlgorithm, SecretKey.SERVER_KEY);
        String jwt = builder.compact();
        return jwt;
    }
}

增加拦截器,对token注解进行全局拦截验证

设置拦截器配置文件:

/**
 * 拦截器配置文件
 *
 * @author AuroraLove
 * @date 2019/3/29
 */
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求,通过判断是否有 @Token 注解
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }

}

新增拦截器

/**
 * 拦截器
 *
 * @author AuroraLove
 * @date 2019/3/29
 */
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //检查是否有token注释,有则认证
        if (method.isAnnotationPresent(Token.class)) {
            Token tokenAnnotation = method.getAnnotation(Token.class);
            if (tokenAnnotation.required()) {
                //从请求头中取出token参数
                String token = httpServletRequest.getHeader("token");
                if (token == null){
                    throw new Exception("token信息不存在!");
                }
                //解析token参数
                Map<String, Claim> claims = JWT.decode(token).getClaims();
                Long id = claims.get("id").asLong();
                //验证用户是否存在
                if (userService.verify(id)){
                    return true;
                }
                throw new Exception("token信息验证失败");
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

验证结果:

伪造失败token

springboot单点登录session_HTTPS_05

成功结果

springboot单点登录session_spring_06