Spring Boot Kotlin Jwt Token

本篇记录了,基于 Spring Boot、Kotlin、Jwt 的 Token 接口认证功能的项目内容

环境如下

Plugin Version
SpringBoot 2.0.8.RELEASE
Kotlin 1.2.71
Java-Jwt 3.3

时序图

kotlin-jwt

项目配置

  • Maven 依赖如下:

    <!-- Spring Boot -->
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <properties>
        <java.version>1.8</java.version>
        <kotlin.version>1.2.71</kotlin.version>
    </properties>
    
    <dependencies>
        <!-- Web -->
        <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.3.0</version>
        </dependency>
        <!-- FastJson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.49</version>
        </dependency>
        <!-- Kotlin -->
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-kotlin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
        </dependency>
    </dependencies>
    
    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
  • JwtUtil.kt 工具类,实现了签名、解析、校验、获取用户名的相关方法.

    package com.raindrop.auth.utils
    
    import com.auth0.jwt.JWT
    import com.auth0.jwt.algorithms.Algorithm
    import com.auth0.jwt.interfaces.Claim
    import java.util.*
    
    /**
    * Jwt 工具类
    *
    * @author Raindrop
    */
    open class JwtUtil {
        companion object {
            /* 秘钥 */
            private val secret = "raindrop-666"
            /* 过期时间 15 分钟 */
            private val expireTime = 15 * 60 * 1000
    
            /**
            * 生成 Token
            *
            * @param username 用户名
            * @return token 令牌
            */
            fun sign(username: String): String {var createDate = Date()
                var expireDate = Date(System.currentTimeMillis() + expireTime)
    
                var claims = mapOf("username" to username)
                return JWT.create()
                        .withHeader(claims)
                        .withSubject(username)
                        .withIssuedAt(createDate)
                        .withExpiresAt(expireDate)
                        .sign(Algorithm.HMAC512(secret))
            }
    
            /**
            * 解析 Token
            *
            * @param token 令牌
            * @return claim
            */
            fun parse(token: String): MutableMap<String, Claim>? {return JWT.decode(token).claims
            }
    
            /**
            * 检验 Token
            *
            * @param token 令牌
            * @return 有效 true 无效 false
            */
            fun verifyToken(token: String): Boolean {
                return try {JWT.require(Algorithm.HMAC512(secret)).build().verify(token)
                    true
                } catch (e: Exception) {false}
            }
    
            /**
            * 获取用户名
            *
            * @param token 令牌
            * @return 用户名
            */
            fun getUserName(token: String) = JWT.decode(token).getClaim("username").asString()
    
            /**
            * 刷新 token
            *
            * @param token 原 token
            * @return 新 token
            */
            fun refresh(token: String): String {var createDate = Date()
                var expireDate = Date(System.currentTimeMillis() + expireTime)
    
                var username = parse(token)!!["sub"]!!.asString()
                var header = mapOf("username" to username)
                return JWT.create()
                        .withHeader(header)
                        .withSubject(username)
                        .withIssuedAt(createDate)
                        .withExpiresAt(expireDate)
                        .sign(Algorithm.HMAC512(secret))
            }
        }
    }
  • AuthInterceptor.kt 拦截器,请求前进行拦截. 如果请求方法为 OPTIONS,则放过不进行拦截,关于 OPTIONS 请求方法,主要是在发送 PUT、DELETE 或 Content-Type application/json 请求前进行 预检 ,浏览器会先询问服务器,当前请求域名是否在服务器允许的范围内,如果允许则浏览器会发出正式的 XMLHttpRequest 请求,否则会报错. 此拦截器对 登录 接口不进行拦截.

    package com.raindrop.auth.interceptors
    
    import com.alibaba.fastjson.JSON
    import com.raindrop.auth.model.ResultEntity
    import com.raindrop.auth.model.buildResultEntity
    import com.raindrop.auth.utils.JwtUtil
    import org.slf4j.LoggerFactory
    import org.springframework.stereotype.Component
    import org.springframework.web.servlet.HandlerInterceptor
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    @Component
    class AuthInterceptor : HandlerInterceptor {private val logger = LoggerFactory.getLogger(this.javaClass)
    
        private val options = "OPTIONS"
    
        override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {if (request.method == options) return true
    
            var authorization = request.getHeader("Authorization")
            if (authorization != null) {var token = authorization.substring(7)
                if (JwtUtil.verifyToken(token)) {logger.info("Auth Success Token Effective...")
                    return true
                }
            }
            logger.info("Auth Fail Token Invalid...")
            response.writer.write(JSON.toJSONString(buildUnauthorizedResultEntity()))
            return false
        }
    
        fun buildUnauthorizedResultEntity(): ResultEntity {
            return buildResultEntity {
                code = 403
                message = "Auth Fail"
            }
        }
    
    }
  • AuthConfig.kt 拦截器配置,此处将 登录 接口过滤,不进行拦截,其他全部接口,全部需要携带 token 并认证通过后,方可访问.

    package com.raindrop.auth.config
    
    import com.raindrop.auth.interceptors.AuthInterceptor
    import org.springframework.context.annotation.Configuration
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
    
    @Configuration
    class AuthConfig : WebMvcConfigurer {
    
        /**
          * 添加拦截器
          */
        override fun addInterceptors(registry: InterceptorRegistry) {var excludePath = listOf("/auth/login", "/auth/guest", "/auth/refresh")
            registry.addInterceptor(AuthInterceptor()).excludePathPatterns(excludePath)
        }
    
    }
  • ResultEntity.kt 通用返回实体,此处使用了 kotlin 高级语法,从而可以使用 dsl 方式构建通用返回实体.

    package com.raindrop.auth.model
    
    import java.io.Serializable
    
    data class ResultEntity(
            var code: Int = 200,
            var message: String = "success",
            var data: Any = ""
    ) : Serializable
    
    fun buildResultEntity(builder: ResultEntity.() -> Unit) = ResultEntity().apply(builder)
  • User.kt 用户实体.

    package com.raindrop.auth.model
    
    class User(
            var username: String,
            var password: String,
            var nickName: String
    )
  • IUserService.kt 用户服务接口.

    package com.raindrop.auth.service
    
    interface IUserService {
    
        /**
          * 登录
          *
          * @param username 用户名
          * @param password 用户密码
          * @return
          */
        fun login(username: String, password: String): Boolean
    
    }
  • UserServiceImpl.kt 用户服务实现.

    package com.raindrop.auth.service.impl
    
    import com.raindrop.auth.service.IUserService
    import org.springframework.stereotype.Service
    
    @Service
    class UserServiceImpl : IUserService {
    
        /**
        * 登录
        *
        * @param username 用户名
        * @param password 用户密码
        * @return
        */
        override fun login(username: String, password: String): Boolean = true
    
    }
  • AuthController.kt 业务 Controller,/login 接口不进行拦截,账号密码正确后,返回 token. /users 接口需要拦截,需认证通过后携带正确的 token 后,方可访问.

    package com.raindrop.auth.web
    
    import com.raindrop.auth.model.ResultEntity
    import com.raindrop.auth.model.User
    import com.raindrop.auth.model.buildResultEntity
    import com.raindrop.auth.service.IUserService
    import com.raindrop.auth.utils.JwtUtil
    import org.slf4j.LoggerFactory
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.web.bind.annotation.GetMapping
    import org.springframework.web.bind.annotation.PostMapping
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RestController
    
    @RestController
    @RequestMapping("/auth")
    class AuthController {private val logger = LoggerFactory.getLogger(this.javaClass)
    
        @Autowired
        lateinit var userService: IUserService
    
        /**
        * 登录
        *
        * @param username 用户名
        * @param password 用户密码
        * @return
        */
        @PostMapping("/login")
        fun login(username: String, password: String): ResultEntity {if (username == null) {logger.warn("Login Fail, UserName Is Null")
                throw IllegalArgumentException(" 登录失败,用户不存在 ")
            }
            if (password == null) {logger.warn("Login Fail, Password Is Null")
                throw IllegalArgumentException(" 登录失败,账号或密码错误 ")
            }
    
            if (userService.login(username, password)) {var token = JwtUtil.sign(username)
                return buildResultEntity {data = token}
            }
    
            return buildResultEntity {
                code = 400
                message = " 登录失败 "
            }
        }
    
        /**
        * 获取用户列表
        *
        * @return
        */
        @GetMapping("/users")
        fun getUserList(): ResultEntity {
            var users = listOf(User("wl", "wl", "wl"),
                    User("lv", "lv", "lv"),
                    User("ee", "ee", "ee")
            )
            return buildResultEntity {data = users}
        }
    
    
    @PostMapping("/refresh")
    fun refreshToken(token: String): String = if (JwtUtil.verifyToken(token)) JwtUtil.refresh(token) else "Token Invalid"

    @GetMapping("/guest")
    fun getGuestMessage(): ResultEntity {
        return buildResultEntity {data = "Hello Guest, You are handsome!"}
    }

}
```
  • AuthTokenApplication.kt 启动类.

    package com.raindrop.auth
    
    import org.springframework.boot.autoconfigure.SpringBootApplication
    import org.springframework.boot.runApplication
    
    @SpringBootApplication
    class AuthTokenApplication
    
    fun main(args: Array<String>) {runApplication<AuthTokenApplication>(*args)
    }

Example



Java     

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!