├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── asset ├── .DS_Store └── imgs │ ├── correctToken.png │ ├── data_interaction.png │ ├── incorrectToken.png │ ├── login.png │ ├── noToken.png │ ├── permission_model.png │ ├── register.png │ └── table_design.png ├── pom.xml └── src ├── main ├── java │ └── spring │ │ └── security │ │ └── jwt │ │ ├── SpringSecurityApplication.java │ │ ├── SpringSecurityContextHelper.java │ │ ├── config │ │ ├── SpringSecurityAuditorAware.java │ │ ├── Swagger2Configuration.java │ │ └── WebCorsConfiguration.java │ │ ├── constant │ │ ├── ErrorConstants.java │ │ ├── SecurityConstants.java │ │ └── UserRoleConstants.java │ │ ├── dto │ │ ├── HeartBeatRecordDTO.java │ │ ├── PageDTO.java │ │ ├── UserDTO.java │ │ ├── UserLoginDTO.java │ │ └── UserRegisterDTO.java │ │ ├── entity │ │ ├── AbstractAuditingEntity.java │ │ ├── HeartBeatRecord.java │ │ ├── User.java │ │ └── UserRole.java │ │ ├── exception │ │ ├── AlreadyExistsException.java │ │ ├── FieldError.java │ │ ├── GlobalExceptionHandler.java │ │ └── ServiceException.java │ │ ├── filter │ │ └── JwtAuthorizationFilter.java │ │ ├── heartbeat │ │ ├── AbstractHeartBeatHandler.java │ │ ├── HeartBeatConfig.java │ │ ├── HeartBeatHandler.java │ │ ├── HeartBeatLogger.java │ │ └── HeartBeatRecordHelper.java │ │ ├── repository │ │ ├── HeartBeatRecordRepository.java │ │ ├── UserRepository.java │ │ └── UserRoleRepository.java │ │ ├── security │ │ ├── JwtUser.java │ │ └── config │ │ │ ├── JwtConfigurer.java │ │ │ └── SecurityConfiguration.java │ │ ├── service │ │ ├── AuthService.java │ │ ├── HeartBeatRecordService.java │ │ ├── UserRoleService.java │ │ ├── UserService.java │ │ └── mapper │ │ │ └── UserMapper.java │ │ ├── util │ │ ├── .DS_Store │ │ ├── IpUtils.java │ │ ├── JwtUtils.java │ │ └── SecurityUtils.java │ │ └── web │ │ └── rest │ │ ├── AuthResource.java │ │ ├── HeartBeatResource.java │ │ └── UserResource.java └── resources │ ├── application-dev.properties │ └── application.properties └── test └── java └── spring └── security └── jwt └── SpringSecurityApplicationTests.java /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 3 | 4 | *.iml 5 | 6 | ## Directory-based project format: 7 | .idea/ 8 | # if you remove the above rule, at least ignore the following: 9 | 10 | # User-specific stuff: 11 | # .idea/workspace.xml 12 | # .idea/tasks.xml 13 | # .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | # .idea/dataSources.ids 17 | # .idea/dataSources.xml 18 | # .idea/sqlDataSources.xml 19 | # .idea/dynamic.xml 20 | # .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | # .idea/gradle.xml 24 | # .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | # .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.ipr 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | ### Maven template 42 | target/ 43 | pom.xml.tag 44 | pom.xml.releaseBackup 45 | pom.xml.versionsBackup 46 | pom.xml.next 47 | release.properties 48 | dependency-reduced-pom.xml 49 | buildNumber.properties 50 | .mvn/timing.properties 51 | 52 | ### Java 53 | 54 | *.class 55 | 56 | # Mobile Tools for Java (J2ME) 57 | .mtj.tmp/ 58 | 59 | # Package Files # 60 | *.jar 61 | *.war 62 | *.ear 63 | 64 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 65 | hs_err_pid* 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Star Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Security With JWT 2 | 3 | ### 概述 4 | 5 | Spring Security 是 Spring 全家桶中一个功能强大且高度可定制的身份验证和访问控制框架。与所有 Spring 项目一样,我们可以轻松扩展 Spring Security 以满足自定义要求。 6 | 7 | 由于 Spring Security 功能十分强大,相比于其他技术来说很难上手,很多刚接触 Spring Security 的开发者很难通过文档或者视频就能将其进行运用到实际开发中。 8 | 9 | 在公司实习的时候接触到的一个项目就使用了 Spring Security 这个强大的安全验证框架来完成用户的登录模块,并且也是自己负责的一个模块。当时自己对 Spring Security 基本不熟悉,可以说是第一次接触,查阅了很多关于这方面的资料,看得似懂非懂的,并且还在导师的指导下都花了将近一周的时间才勉强完成。 10 | 11 | Spring Security 对于初学者来说,的确很难上手。于是自己在工作之余对这部分知识进行了学习,并实现了一个简单的项目,主要使用了 Spring Boot 技术集成 Spring Security 和 Spring Data Jpa 技术。这个项目实现的比较简单,还有很多地方需要优化,希望有兴趣的朋友可以一起完善,期待你的 PR。 12 | 13 | ### 项目调整记录 14 | 15 | - 增加 Swagger UI,方便查看项目接口。 16 | - 增加全局异常捕获功能。 17 | - 增加 JPA 审计功能,完善数据表审计信息。 18 | - 在 Controller 层中暴露用户登录接口(`/api/auth/login`)。 19 | - 增加 Token 刷新功能,提供前端和后端各自的方案。 20 | 21 | ### Token 刷新并发处理方案 22 | 23 | - 后端利用 Redis 缓存:https://github.com/yifanzheng/spring-security-jwt/tree/refresh-token-redis 24 | 25 | - 前端利用请求拦截器:https://github.com/yifanzheng/spring-security-jwt/tree/refresh-token-frontend 26 | 27 | 28 | ### 项目下载 29 | 30 | - git clone https://github.com/yifanzheng/spring-security-jwt.git 。 31 | 32 | - 配置好 Maven 仓库,使用 IntelliJ IDEA 工具打开项目。 33 | 34 | - 在 application.properties 配置文件中将数据库信息改成你自己的。 35 | 36 | ### 权限控制 37 | 38 | 本 Demo 权限控制采用 RBAC 思想。简单地说,一个用户拥有若干角色,用户与角色形成多对多关系。 39 | 40 | **模型** 41 | ![权限模型](https://img-blog.csdnimg.cn/20200628230812139.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 42 | 43 | **数据表设计** 44 | 45 | 用户表与用户角色表是多对多的关系。因为这里比较简单,所以表设计上有点冗余。小伙伴们可以根据实际情况重新设计。 46 | ![表设计](https://img-blog.csdnimg.cn/2020062823082793.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 47 | 48 | **数据交互** 49 | 50 | 用户登录 -> 后端验证登录并返回 token -> 前端携带 token 请求后端数据 -> 后端返回数据。 51 | 52 | ![token流程](https://user-images.githubusercontent.com/22571230/138224284-770c1891-1be4-4c7d-9b76-c6b821f4e0b2.png) 53 | 54 | 55 | ### 项目核心类说明 56 | 57 | **WebCorsConfiguration** 58 | 59 | WebCorsConfiguration 配置类,主要解决 HTTP 请求跨域问题。这里需要注意的是,如果没有将 `Authorization` 头字段暴露给客户端的话,客户端是无法获取到 Token 信息的。 60 | 61 | ```java 62 | /** 63 | * WebCorsConfiguration 跨域配置 64 | * 65 | * @author star 66 | */ 67 | @Configuration 68 | public class WebCorsConfiguration implements WebMvcConfigurer { 69 | 70 | /** 71 | * 设置swagger为默认主页 72 | */ 73 | @Override 74 | public void addViewControllers(ViewControllerRegistry registry) { 75 | registry.addViewController("/").setViewName("redirect:/swagger-ui.html"); 76 | registry.setOrder(Ordered.HIGHEST_PRECEDENCE); 77 | WebMvcConfigurer.super.addViewControllers(registry); 78 | } 79 | 80 | @Bean 81 | public CorsFilter corsFilter() { 82 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 83 | CorsConfiguration config = new CorsConfiguration(); 84 | config.setAllowCredentials(true); 85 | config.setAllowedOrigins(Collections.singletonList("*")); 86 | config.setAllowedMethods(Collections.singletonList("*")); 87 | config.setAllowedHeaders(Collections.singletonList("*")); 88 | // 暴露 header 中的其他属性给客户端应用程序 89 | config.setExposedHeaders(Arrays.asList( 90 | "Authorization", "X-Total-Count", "Link", 91 | "Access-Control-Allow-Origin", 92 | "Access-Control-Allow-Credentials" 93 | )); 94 | source.registerCorsConfiguration("/**", config); 95 | return new CorsFilter(source); 96 | } 97 | 98 | } 99 | ``` 100 | 101 | **SecurityConfiguration** 102 | 103 | SecurityConfiguration 配置类继承了 Spring Security 的 WebSecurityConfigurerAdapter 类。WebSecurityConfigurerAdapter 类提供了默认的安全配置,并允许其他类通过覆盖其方法来扩展它并自定义安全配置。 104 | 105 | 这里配置了如下内容: 106 | 107 | - 忽略某些不需要验证的就能访问的资源路径; 108 | 109 | - 在 Spring Security 机制中配置需要验证后才能访问的资源路径、不需要验证就可以访问的资源路径以及指定某些资源只能被特定角色访问。 110 | 111 | - 配置请求权限认证异常时的处理类; 112 | 113 | - 将自定义的 `JwtAuthorizationFilter` 过滤器添加到 Spring Security 机制中。 114 | 115 | ```java 116 | 117 | /** 118 | * Web 安全配置 119 | * 120 | * @author star 121 | **/ 122 | @Configuration 123 | @EnableWebSecurity 124 | @EnableGlobalMethodSecurity(prePostEnabled = true) 125 | @Import(SecurityProblemSupport.class) 126 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 127 | 128 | @Autowired 129 | private CorsFilter corsFilter; 130 | 131 | @Autowired 132 | private SecurityProblemSupport securityProblemSupport; 133 | 134 | /** 135 | * 使用 Spring Security 推荐的加密方式进行登录密码的加密 136 | */ 137 | @Bean 138 | public BCryptPasswordEncoder bCryptPasswordEncoder(){ 139 | return new BCryptPasswordEncoder(); 140 | } 141 | 142 | /** 143 | * 此方法配置的资源路径不会进入 Spring Security 机制进行验证 144 | */ 145 | @Override 146 | public void configure(WebSecurity web) { 147 | web.ignoring() 148 | .antMatchers(HttpMethod.OPTIONS, "/**") 149 | .antMatchers("/app/**/*.{js,html}") 150 | .antMatchers("/v2/api-docs/**") 151 | .antMatchers("/i18n/**") 152 | .antMatchers("/test/**") 153 | .antMatchers("/content/**") 154 | .antMatchers("/webjars/springfox-swagger-ui/**") 155 | .antMatchers("/swagger-resources/**") 156 | .antMatchers("/swagger-ui.html"); 157 | } 158 | 159 | /** 160 | * 定义安全策略,设置 HTTP 访问规则 161 | */ 162 | @Override 163 | protected void configure(HttpSecurity http) throws Exception { 164 | http 165 | .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) 166 | .exceptionHandling() 167 | // 当用户无权访问资源时发送 401 响应 168 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 169 | // 当用户访问资源因权限不足时发送 403 响应 170 | .accessDeniedHandler(securityProblemSupport) 171 | .and() 172 | // 禁用 CSRF 173 | .csrf().disable() 174 | .headers().frameOptions().disable() 175 | .and() 176 | .logout().logoutUrl("/auth/logout").and() 177 | .authorizeRequests() 178 | // 指定路径下的资源需要进行验证后才能访问 179 | .antMatchers("/").permitAll() 180 | // 配置登录地址 181 | .antMatchers(HttpMethod.POST, SecurityConstants.AUTH_LOGIN_URL).permitAll() 182 | .antMatchers(HttpMethod.POST,"/api/users/register").permitAll() 183 | // 其他请求需验证 184 | .anyRequest().authenticated() 185 | .and() 186 | // 不需要 session(不创建会话) 187 | .sessionManagement() 188 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 189 | .and() 190 | .apply(securityConfigurationAdapter()); 191 | super.configure(http); 192 | } 193 | 194 | private JwtConfigurer securityConfigurationAdapter() throws Exception{ 195 | return new JwtConfigurer(new JwtAuthorizationFilter(authenticationManager())); 196 | } 197 | } 198 | ``` 199 | 200 | **JwtAuthorizationFilter** 201 | 202 | JwtAuthorizationFilter 用户请求授权过滤器,用于从用户请求中获取 token 信息,并对其进行验证,同时加载与 token 相关联的用户身份认证信息,并添加到 Spring Security 上下文中。 203 | 204 | ```java 205 | /** 206 | * JwtAuthorizationFilter 用户请求授权过滤器 207 | * 208 | *

209 | * 提供请求授权功能。用于处理所有 HTTP 请求,并检查是否存在带有正确 token 的 Authorization 标头。 210 | * 如果 token 有效,则过滤器会将身份验证数据添加到 Spring 的安全上下文中,并授权此次请求访问资源。

211 | * 212 | * @author star 213 | */ 214 | public class JwtAuthorizationFilter extends BasicAuthenticationFilter { 215 | 216 | public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { 217 | super(authenticationManager); 218 | } 219 | 220 | @Override 221 | protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { 222 | // 从 HTTP 请求中获取 token 223 | String token = this.getTokenFromHttpRequest(request); 224 | // 验证 token 是否有效 225 | if (StringUtils.hasText(token) && JwtUtils.validateToken(token)) { 226 | // 获取认证信息 227 | Authentication authentication = JwtUtils.getAuthentication(token); 228 | // 将认证信息存入 Spring 安全上下文中 229 | SecurityContextHolder.getContext().setAuthentication(authentication); 230 | } 231 | // 放行请求 232 | filterChain.doFilter(request, response); 233 | 234 | } 235 | 236 | /** 237 | * 从 HTTP 请求中获取 token 238 | * 239 | * @param request HTTP 请求 240 | * @return 返回 token 241 | */ 242 | private String getTokenFromHttpRequest(HttpServletRequest request) { 243 | String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER); 244 | if (authorization == null || !authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) { 245 | return null; 246 | } 247 | // 去掉 token 前缀 248 | return authorization.replace(SecurityConstants.TOKEN_PREFIX, ""); 249 | } 250 | 251 | } 252 | ``` 253 | 254 | **所有的用户请求**都会经过此过滤器,当请求进入过滤器后会经历如下步骤: 255 | 256 | - 首先,从请求中获取 token 信息,并检查 token 的有效性。 257 | 258 | - 如果 token 有效,则解析 token 获取用户名,然后使用用户名从数据库中获取用户角色信息,并在 Spring Security 的上下文中设置身份验证。 259 | 260 | - 如果 token 无效或请求不带 token 信息,则直接放行。 261 | 262 | 特别说明,这里用户的角色信息,是从数据库中重新获取的。其实,这里也可以换成从 token 信息中解析出用户角色,这样可以避免直接访问数据库。 263 | 264 | 但是,直接从数据库获取用户信息也是很有帮助的。例如,如果用户角色已更改,则可能要禁止使用此 token 进行访问。 265 | 266 | **JwtUtils** 267 | 268 | JwtUtils 工具类,在用户登录成功后,主要用于生成 token,并验证用户请求中发送的 token。 269 | 270 | ```java 271 | /** 272 | * Jwt 工具类,用于生成、解析与验证 token 273 | * 274 | * @author star 275 | **/ 276 | public final class JwtUtils { 277 | 278 | private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); 279 | 280 | private static final byte[] secretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); 281 | 282 | private JwtUtils() { 283 | throw new IllegalStateException("Cannot create instance of static util class"); 284 | } 285 | 286 | /** 287 | * 根据用户名和用户角色生成 token 288 | * 289 | * @param userName 用户名 290 | * @param roles 用户角色 291 | * @param isRemember 是否记住我 292 | * @return 返回生成的 token 293 | */ 294 | public static String generateToken(String userName, List roles, boolean isRemember) { 295 | byte[] jwtSecretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); 296 | // 过期时间 297 | long expiration = isRemember ? SecurityConstants.EXPIRATION_REMEMBER_TIME : SecurityConstants.EXPIRATION_TIME; 298 | // 生成 token 299 | String token = Jwts.builder() 300 | // 生成签证信息 301 | .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) 302 | .signWith(Keys.hmacShaKeyFor(jwtSecretKey), SignatureAlgorithm.HS256) 303 | .setSubject(userName) 304 | .claim(SecurityConstants.TOKEN_ROLE_CLAIM, roles) 305 | .setIssuer(SecurityConstants.TOKEN_ISSUER) 306 | .setIssuedAt(new Date()) 307 | .setAudience(SecurityConstants.TOKEN_AUDIENCE) 308 | // 设置有效时间 309 | .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) 310 | .compact(); 311 | return token; 312 | } 313 | 314 | /** 315 | * 验证 token 是否有效 316 | * 317 | *

318 | * 如果解析失败,说明 token 是无效的 319 | * 320 | * @param token token 信息 321 | * @return 如果返回 true,说明 token 有效 322 | */ 323 | public static boolean validateToken(String token) { 324 | try { 325 | getTokenBody(token); 326 | return true; 327 | } catch (ExpiredJwtException e) { 328 | logger.warn("Request to parse expired JWT : {} failed : {}", token, e.getMessage()); 329 | } catch (UnsupportedJwtException e) { 330 | logger.warn("Request to parse unsupported JWT : {} failed : {}", token, e.getMessage()); 331 | } catch (MalformedJwtException e) { 332 | logger.warn("Request to parse invalid JWT : {} failed : {}", token, e.getMessage()); 333 | } catch (IllegalArgumentException e) { 334 | logger.warn("Request to parse empty or null JWT : {} failed : {}", token, e.getMessage()); 335 | } 336 | return false; 337 | } 338 | 339 | /** 340 | * 根据 token 获取用户认证信息 341 | * 342 | * @param token token 信息 343 | * @return 返回用户认证信息 344 | */ 345 | public static Authentication getAuthentication(String token) { 346 | Claims claims = getTokenBody(token); 347 | // 获取用户角色字符串 348 | List roles = (List)claims.get(SecurityConstants.TOKEN_ROLE_CLAIM); 349 | List authorities = 350 | Objects.isNull(roles) ? Collections.singletonList(new SimpleGrantedAuthority(UserRoleConstants.ROLE_USER)) : 351 | roles.stream() 352 | .map(SimpleGrantedAuthority::new) 353 | .collect(Collectors.toList()); 354 | // 获取用户名 355 | String userName = claims.getSubject(); 356 | 357 | return new UsernamePasswordAuthenticationToken(userName, token, authorities); 358 | 359 | } 360 | 361 | private static Claims getTokenBody(String token) { 362 | return Jwts.parser() 363 | .setSigningKey(secretKey) 364 | .parseClaimsJws(token) 365 | .getBody(); 366 | } 367 | } 368 | ``` 369 | 370 | ### 请求认证流程说明 371 | 372 | 由于已将登录接口暴露在了 Controller 层,请求认证过程将变成,所有的请求会先经过 `JwtAuthorizationFilter` 过滤器,然后进入 Spring Security 机制中。 373 | 374 | ### 测试 API 375 | 376 | **注册账号** 377 | ![register](https://img-blog.csdnimg.cn/20200524195659308.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 378 | **登录** 379 | ![login](https://img-blog.csdnimg.cn/20200524195721904.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 380 | **带上正确的 token 访问需要身份验证的资源** 381 | ![correctToken](https://img-blog.csdnimg.cn/20200524200233825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 382 | **带上不正确的 token 访问需要身份验证的资源** 383 | ![incorrectToken](https://img-blog.csdnimg.cn/20200524200307326.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 384 | 385 | **不带 token 访问需要身份验证的资源** 386 | ![noToken](https://img-blog.csdnimg.cn/20200524200408739.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L29zY2hpbmFfNDE3OTA5MDU=,size_16,color_FFFFFF,t_70) 387 | 388 | 389 | ### 参考文档 390 | 391 | - [https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/ 392 | ](https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/ 393 | ) 394 | - [https://segmentfault.com/a/1190000009231329](https://segmentfault.com/a/1190000009231329) 395 | 396 | - [https://www.springcloud.cc/spring-security.html](https://www.springcloud.cc/spring-security.html) 397 | -------------------------------------------------------------------------------- /asset/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/.DS_Store -------------------------------------------------------------------------------- /asset/imgs/correctToken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/correctToken.png -------------------------------------------------------------------------------- /asset/imgs/data_interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/data_interaction.png -------------------------------------------------------------------------------- /asset/imgs/incorrectToken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/incorrectToken.png -------------------------------------------------------------------------------- /asset/imgs/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/login.png -------------------------------------------------------------------------------- /asset/imgs/noToken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/noToken.png -------------------------------------------------------------------------------- /asset/imgs/permission_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/permission_model.png -------------------------------------------------------------------------------- /asset/imgs/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/register.png -------------------------------------------------------------------------------- /asset/imgs/table_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/asset/imgs/table_design.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | spring-security-jwt 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | security 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.7.0 18 | 19 | 20 | 21 | 22 | 23 | spring 24 | https://maven.aliyun.com/repository/spring 25 | 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | UTF-8 37 | UTF-8 38 | 1.8 39 | 0.22.5 40 | 0.10.7 41 | 2.10.5 42 | 2.1.0 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-web 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-security 62 | 63 | 64 | org.zalando 65 | problem-spring-web 66 | ${problem-spring-web.version} 67 | 68 | 69 | com.h2database 70 | h2 71 | 2.1.210 72 | runtime 73 | 74 | 75 | mysql 76 | mysql-connector-java 77 | runtime 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-data-jpa 82 | 83 | 84 | 85 | com.github.ulisesbocchio 86 | jasypt-spring-boot-starter 87 | ${jasypt.version} 88 | 89 | 90 | 91 | io.jsonwebtoken 92 | jjwt-api 93 | ${jwt.version} 94 | 95 | 96 | io.jsonwebtoken 97 | jjwt-impl 98 | ${jwt.version} 99 | runtime 100 | 101 | 102 | io.jsonwebtoken 103 | jjwt-jackson 104 | ${jwt.version} 105 | runtime 106 | 107 | 108 | 109 | io.springfox 110 | springfox-swagger2 111 | ${springfox.version} 112 | 113 | 114 | io.springfox 115 | springfox-swagger-ui 116 | ${springfox.version} 117 | 118 | 119 | io.springfox 120 | springfox-spring-webmvc 121 | ${springfox.version} 122 | 123 | 124 | org.springframework.plugin 125 | spring-plugin-core 126 | 2.0.0.RELEASE 127 | 128 | 129 | org.springframework.plugin 130 | spring-plugin-metadata 131 | 2.0.0.RELEASE 132 | 133 | 134 | junit 135 | junit 136 | 4.13.2 137 | test 138 | 139 | 140 | 141 | org.apache.commons 142 | commons-lang3 143 | 144 | 145 | org.springframework.data 146 | spring-data-commons 147 | 148 | 149 | com.google.guava 150 | guava 151 | 31.1-jre 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | org.springframework.boot 160 | spring-boot-maven-plugin 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/SpringSecurityApplication.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 9 | import org.springframework.transaction.annotation.EnableTransactionManagement; 10 | 11 | import java.net.InetAddress; 12 | 13 | @EnableTransactionManagement 14 | @EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") 15 | @SpringBootApplication 16 | public class SpringSecurityApplication { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(SpringSecurityApplication.class); 19 | 20 | public static void main(String[] args) { 21 | SpringApplication app = new SpringApplication(SpringSecurityApplication.class); 22 | Environment env = app.run(args).getEnvironment(); 23 | String protocol = "http"; 24 | if (env.getProperty("server.ssl.key-store") != null) { 25 | protocol = "https"; 26 | } 27 | String hostAddress = "localhost"; 28 | try { 29 | hostAddress = InetAddress.getLocalHost().getHostAddress(); 30 | } catch (Exception e) { 31 | log.warn("The host name could not be determined, using `localhost` as fallback."); 32 | } 33 | 34 | log.info("\n----------------------------------------------------------\n\t" 35 | + "Application '{}' is running! Access URLs:\n\t" 36 | + "Local: \t\t{}://localhost:{}\n\t" 37 | + "External: \t{}://{}:{}\n\t" 38 | + "Profile(s): \t{}" 39 | + "\n----------------------------------------------------------", 40 | env.getProperty("spring.application.name"), 41 | protocol, 42 | env.getProperty("server.port"), 43 | protocol, 44 | hostAddress, 45 | env.getProperty("server.port"), 46 | env.getActiveProfiles()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/SpringSecurityContextHelper.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * SpringSecurityContextHelper 10 | * 11 | * @author star 12 | */ 13 | @Component 14 | public class SpringSecurityContextHelper implements ApplicationContextAware { 15 | 16 | private static ApplicationContext applicationContext; 17 | 18 | @Override 19 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 20 | SpringSecurityContextHelper.applicationContext = applicationContext; 21 | } 22 | 23 | /** 24 | * 根据一个 bean 的类型获取相应的 bean 25 | */ 26 | public static T getBean(Class requiredType) { 27 | if (applicationContext == null) { 28 | return null; 29 | } 30 | return applicationContext.getBean(requiredType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/config/SpringSecurityAuditorAware.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.domain.AuditorAware; 5 | import spring.security.jwt.util.SecurityUtils; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * 监听 @CreateBy @LastModifiedBy 自动注入用户名 11 | * 12 | * @author star 13 | **/ 14 | @Configuration 15 | public class SpringSecurityAuditorAware implements AuditorAware { 16 | 17 | @Override 18 | public Optional getCurrentAuditor() { 19 | Optional userLogin = SecurityUtils.getCurrentUserLogin(); 20 | if (userLogin.isPresent()) { 21 | return userLogin; 22 | } 23 | return Optional.of("system"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/config/Swagger2Configuration.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.ApiInfoBuilder; 6 | import springfox.documentation.builders.PathSelectors; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.service.ApiInfo; 9 | import springfox.documentation.service.Contact; 10 | import springfox.documentation.spi.DocumentationType; 11 | import springfox.documentation.spring.web.plugins.Docket; 12 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; 13 | 14 | /** 15 | * Swagger 配置 16 | * 17 | * @author star 18 | */ 19 | @Configuration 20 | @EnableSwagger2WebMvc 21 | public class Swagger2Configuration { 22 | 23 | @Bean 24 | public Docket createRestApi() { 25 | return new Docket(DocumentationType.SWAGGER_2) 26 | .apiInfo(apiInfo()) 27 | .select() 28 | .apis(RequestHandlerSelectors.basePackage("spring.security.jwt.web.rest")) 29 | .paths(PathSelectors.any()) 30 | .build(); 31 | } 32 | 33 | private ApiInfo apiInfo() { 34 | return new ApiInfoBuilder() 35 | .title("Spring Security JWT") 36 | .contact(new Contact("Star.Y.Zheng", "http://yifanstar.top/", "zhengyifan1996@outlook.com")) 37 | .version("1.0") 38 | .build(); 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/config/WebCorsConfiguration.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.Ordered; 6 | import org.springframework.web.cors.CorsConfiguration; 7 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 8 | import org.springframework.web.filter.CorsFilter; 9 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 10 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | import java.util.Arrays; 14 | import java.util.Collections; 15 | 16 | /** 17 | * WebCorsConfiguration 跨域配置 18 | * 19 | * @author star 20 | */ 21 | @Configuration 22 | public class WebCorsConfiguration implements WebMvcConfigurer { 23 | 24 | /** 25 | * 设置swagger为默认主页 26 | */ 27 | @Override 28 | public void addViewControllers(ViewControllerRegistry registry) { 29 | registry.addViewController("/").setViewName("redirect:/swagger-ui.html"); 30 | registry.setOrder(Ordered.HIGHEST_PRECEDENCE); 31 | WebMvcConfigurer.super.addViewControllers(registry); 32 | } 33 | 34 | @Bean 35 | public CorsFilter corsFilter() { 36 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 37 | CorsConfiguration config = new CorsConfiguration(); 38 | config.setAllowCredentials(true); 39 | config.setAllowedOrigins(Collections.singletonList("*")); 40 | config.setAllowedMethods(Collections.singletonList("*")); 41 | config.setAllowedHeaders(Collections.singletonList("*")); 42 | // 暴露 header 中的其他属性给客户端应用程序 43 | config.setExposedHeaders(Arrays.asList( 44 | "Authorization", "X-Total-Count", "Link", 45 | "Access-Control-Allow-Origin", 46 | "Access-Control-Allow-Credentials" 47 | )); 48 | source.registerCorsConfiguration("/**", config); 49 | return new CorsFilter(source); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/constant/ErrorConstants.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.constant; 2 | 3 | import java.net.URI; 4 | 5 | /** 6 | * ErrorConstants 7 | * 8 | * @author star 9 | */ 10 | public final class ErrorConstants { 11 | private ErrorConstants() { 12 | throw new IllegalStateException("Cannot create instance of static constant class"); 13 | } 14 | 15 | 16 | public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure"; 17 | public static final String ERR_VALIDATION = "error.validation"; 18 | public static final URI DEFAULT_TYPE = null; 19 | 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/constant/SecurityConstants.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.constant; 2 | 3 | /** 4 | * SecurityConstants 5 | * 6 | * @author star 7 | **/ 8 | public final class SecurityConstants { 9 | 10 | private SecurityConstants() { 11 | throw new IllegalStateException("Cannot create instance of static constant class"); 12 | } 13 | 14 | /** 15 | * 用于登录的 url 16 | */ 17 | public static final String AUTH_LOGIN_URL = "/api/auth/login"; 18 | 19 | /** 20 | * JWT签名密钥,这里使用 HS512 算法的签名密钥 21 | *

22 | * 注意:最好使用环境变量或 .properties 文件的方式将密钥传入程序 23 | * 密钥生成地址:http://www.allkeysgenerator.com/ 24 | */ 25 | public static final String JWT_SECRET_KEY = "p2s5v8y/B?E(H+MbQeThVmYq3t6w9z$C&F)J@NcRfUjXnZr4u7x!A%D*G-KaPdS"; 26 | 27 | 28 | /** 29 | * 一般是在请求头里加入 Authorization,并加上 Bearer 标注 30 | */ 31 | public static final String TOKEN_PREFIX = "Bearer "; 32 | 33 | /** 34 | * Authorization 请求头 35 | */ 36 | public static final String TOKEN_HEADER = "Authorization"; 37 | 38 | /** 39 | * token 类型 40 | */ 41 | public static final String TOKEN_TYPE = "JWT"; 42 | 43 | public static final String TOKEN_ROLE_CLAIM = "role"; 44 | public static final String TOKEN_ISSUER = "security"; 45 | public static final String TOKEN_AUDIENCE = "security-all"; 46 | 47 | /** 48 | * 当 Remember 是 false 时,token 有效时间 2 小时 49 | */ 50 | public static final long EXPIRATION_TIME = 60 * 60 * 2L; 51 | 52 | /** 53 | * 当 Remember 是 true 时,token 有效时间 7 天 54 | */ 55 | public static final long EXPIRATION_REMEMBER_TIME = 60 * 60 * 24 * 7L; 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/constant/UserRoleConstants.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.constant; 2 | 3 | /** 4 | * RoleConstants 5 | * 6 | * @author star 7 | */ 8 | public final class UserRoleConstants { 9 | 10 | private UserRoleConstants() { 11 | throw new IllegalStateException("Cannot create instance of static constant class"); 12 | } 13 | 14 | public static final String ROLE_USER = "ROLE_USER"; 15 | 16 | public static final String ROLE_ADMIN = "ROLE_ADMIN"; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/dto/HeartBeatRecordDTO.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.dto; 2 | 3 | import java.time.Instant; 4 | 5 | /** 6 | * HeartBeatRecordDTO 7 | * 8 | * @author star 9 | */ 10 | public class HeartBeatRecordDTO { 11 | 12 | /** 13 | * 项目名字 14 | */ 15 | private String projectPath; 16 | 17 | /** 18 | * 服务器ip 19 | */ 20 | private String serverIp; 21 | 22 | /** 23 | * 进程号 24 | */ 25 | private Integer processNum; 26 | 27 | /** 28 | * 进程开启时间 29 | */ 30 | private Instant processStartTime; 31 | 32 | /** 33 | * 心跳时间 34 | */ 35 | private Instant heartBeatTime; 36 | 37 | public String getProjectPath() { 38 | return projectPath; 39 | } 40 | 41 | public void setProjectPath(String projectPath) { 42 | this.projectPath = projectPath; 43 | } 44 | 45 | public String getServerIp() { 46 | return serverIp; 47 | } 48 | 49 | public void setServerIp(String serverIp) { 50 | this.serverIp = serverIp; 51 | } 52 | 53 | public Integer getProcessNum() { 54 | return processNum; 55 | } 56 | 57 | public void setProcessNum(Integer processNum) { 58 | this.processNum = processNum; 59 | } 60 | 61 | public Instant getProcessStartTime() { 62 | return processStartTime; 63 | } 64 | 65 | public void setProcessStartTime(Instant processStartTime) { 66 | this.processStartTime = processStartTime; 67 | } 68 | 69 | public Instant getHeartBeatTime() { 70 | return heartBeatTime; 71 | } 72 | 73 | public void setHeartBeatTime(Instant heartBeatTime) { 74 | this.heartBeatTime = heartBeatTime; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/dto/PageDTO.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.dto; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author star 7 | */ 8 | public class PageDTO { 9 | 10 | private int totalPage; 11 | 12 | private List data; 13 | 14 | public static PageDTO of(int totalPage, List data) { 15 | PageDTO tPageDTO = new PageDTO<>(); 16 | tPageDTO.setTotalPage(totalPage); 17 | tPageDTO.setData(data); 18 | 19 | return tPageDTO; 20 | } 21 | 22 | public int getTotalPage() { 23 | return totalPage; 24 | } 25 | 26 | public void setTotalPage(int totalPage) { 27 | this.totalPage = totalPage; 28 | } 29 | 30 | public List getData() { 31 | return data; 32 | } 33 | 34 | public void setData(List data) { 35 | this.data = data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.dto; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * UserDTO 7 | * 8 | * @author star 9 | */ 10 | public class UserDTO { 11 | 12 | private String userName; 13 | 14 | private String nickName; 15 | 16 | private String email; 17 | 18 | private List roles; 19 | 20 | public String getUserName() { 21 | return userName; 22 | } 23 | 24 | public void setUserName(String userName) { 25 | this.userName = userName; 26 | } 27 | 28 | public String getNickName() { 29 | return nickName; 30 | } 31 | 32 | public void setNickName(String nickName) { 33 | this.nickName = nickName; 34 | } 35 | 36 | public String getEmail() { 37 | return email; 38 | } 39 | 40 | public void setEmail(String email) { 41 | this.email = email; 42 | } 43 | 44 | public List getRoles() { 45 | return roles; 46 | } 47 | 48 | public void setRoles(List roles) { 49 | this.roles = roles; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/dto/UserLoginDTO.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | /** 6 | * UserLoginDTO 7 | * 8 | * @author star 9 | */ 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | public class UserLoginDTO { 12 | 13 | private String userName; 14 | 15 | private String password; 16 | 17 | /** 18 | * 是否记住我,默认 false 19 | */ 20 | private Boolean rememberMe = false; 21 | 22 | public String getUserName() { 23 | return userName; 24 | } 25 | 26 | public void setUserName(String userName) { 27 | this.userName = userName; 28 | } 29 | 30 | public String getPassword() { 31 | return password; 32 | } 33 | 34 | public void setPassword(String password) { 35 | this.password = password; 36 | } 37 | 38 | public Boolean getRememberMe() { 39 | return rememberMe; 40 | } 41 | 42 | public void setRememberMe(Boolean rememberMe) { 43 | this.rememberMe = rememberMe; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/dto/UserRegisterDTO.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.dto; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotBlank; 5 | import javax.validation.constraints.Size; 6 | 7 | /** 8 | * UserDTO 9 | * 10 | * @author star 11 | **/ 12 | public class UserRegisterDTO { 13 | 14 | @NotBlank 15 | @Size(min = 4, max = 30) 16 | private String userName; 17 | 18 | @NotBlank 19 | @Size(min = 4, max = 30) 20 | private String nickName; 21 | 22 | @NotBlank 23 | @Size(min = 6, max = 15) 24 | private String password; 25 | 26 | @NotBlank 27 | @Email(message = "邮箱格式不对") 28 | @Size(max = 40) 29 | private String email; 30 | 31 | public String getUserName() { 32 | return userName; 33 | } 34 | 35 | public void setUserName(String userName) { 36 | this.userName = userName; 37 | } 38 | 39 | public String getNickName() { 40 | return nickName; 41 | } 42 | 43 | public void setNickName(String nickName) { 44 | this.nickName = nickName; 45 | } 46 | 47 | public String getPassword() { 48 | return password; 49 | } 50 | 51 | public void setPassword(String password) { 52 | this.password = password; 53 | } 54 | 55 | public String getEmail() { 56 | return email; 57 | } 58 | 59 | public void setEmail(String email) { 60 | this.email = email; 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/entity/AbstractAuditingEntity.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.entity; 2 | 3 | import org.springframework.data.annotation.CreatedBy; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedBy; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.Column; 10 | import javax.persistence.EntityListeners; 11 | import javax.persistence.MappedSuperclass; 12 | import java.io.Serializable; 13 | import java.time.Instant; 14 | 15 | /** 16 | * 抽象实体类 17 | * 18 | * @author star 19 | */ 20 | @MappedSuperclass 21 | @EntityListeners(AuditingEntityListener.class) 22 | public abstract class AbstractAuditingEntity implements Serializable { 23 | 24 | private static final long serialVersionUID = -8707034280923250254L; 25 | 26 | @CreatedBy 27 | @Column(name = "create_by", length = 210, nullable = false) 28 | private String createBy; 29 | 30 | @CreatedDate 31 | @Column(name = "create_date", nullable = false) 32 | private Instant createDate = Instant.now(); 33 | 34 | @LastModifiedBy 35 | @Column(name = "last_modified_by", length = 20) 36 | private String lastModifiedBy; 37 | 38 | @LastModifiedDate 39 | @Column(name = "last_modified_date") 40 | private Instant lastModifiedDate = Instant.now(); 41 | 42 | public String getCreateBy() { 43 | return createBy; 44 | } 45 | 46 | public void setCreateBy(String createBy) { 47 | this.createBy = createBy; 48 | } 49 | 50 | public Instant getCreateDate() { 51 | return createDate; 52 | } 53 | 54 | public void setCreateDate(Instant createDate) { 55 | this.createDate = createDate; 56 | } 57 | 58 | public String getLastModifiedBy() { 59 | return lastModifiedBy; 60 | } 61 | 62 | public void setLastModifiedBy(String lastModifiedBy) { 63 | this.lastModifiedBy = lastModifiedBy; 64 | } 65 | 66 | public Instant getLastModifiedDate() { 67 | return lastModifiedDate; 68 | } 69 | 70 | public void setLastModifiedDate(Instant lastModifiedDate) { 71 | this.lastModifiedDate = lastModifiedDate; 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/entity/HeartBeatRecord.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.entity; 2 | 3 | import javax.persistence.*; 4 | import java.time.Instant; 5 | 6 | /** 7 | * 心跳记录 8 | * 9 | * @author star 10 | */ 11 | @Entity 12 | @Table(name = "heart_beat_record") 13 | public class HeartBeatRecord extends AbstractAuditingEntity { 14 | 15 | private static final long serialVersionUID = 2986726195041013116L; 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @Column(name = "project_path", columnDefinition = "varchar(255)") 22 | private String projectPath; 23 | 24 | @Column(name = "server_ip", columnDefinition = "varchar(15)") 25 | private String serverIp; 26 | 27 | @Column(name = "process_num") 28 | private Integer processNum; 29 | 30 | /** 31 | * 进程开启时间 32 | */ 33 | @Column(name = "process_start_time") 34 | private Instant processStartTime; 35 | 36 | /** 37 | * 心跳时间 38 | */ 39 | @Column(name = "heart_beat_time") 40 | private Instant heartBeatTime; 41 | 42 | public Long getId() { 43 | return id; 44 | } 45 | 46 | public void setId(Long id) { 47 | this.id = id; 48 | } 49 | 50 | public String getProjectPath() { 51 | return projectPath; 52 | } 53 | 54 | public void setProjectPath(String projectPath) { 55 | this.projectPath = projectPath; 56 | } 57 | 58 | public String getServerIp() { 59 | return serverIp; 60 | } 61 | 62 | public void setServerIp(String serverIp) { 63 | this.serverIp = serverIp; 64 | } 65 | 66 | public Integer getProcessNum() { 67 | return processNum; 68 | } 69 | 70 | public void setProcessNum(Integer processNum) { 71 | this.processNum = processNum; 72 | } 73 | 74 | public Instant getProcessStartTime() { 75 | return processStartTime; 76 | } 77 | 78 | public void setProcessStartTime(Instant processStartTime) { 79 | this.processStartTime = processStartTime; 80 | } 81 | 82 | public Instant getHeartBeatTime() { 83 | return heartBeatTime; 84 | } 85 | 86 | public void setHeartBeatTime(Instant heartBeatTime) { 87 | this.heartBeatTime = heartBeatTime; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/entity/User.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.entity; 2 | 3 | import javax.persistence.*; 4 | 5 | /** 6 | * User 7 | * 8 | * @author star 9 | */ 10 | @Entity 11 | @Table(name = "user") 12 | public class User extends AbstractAuditingEntity { 13 | 14 | private static final long serialVersionUID = 3340373364530753417L; 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private Long id; 19 | 20 | @Column(name = "user_name", columnDefinition="varchar(30)", nullable = false, unique = true) 21 | private String userName; 22 | 23 | @Column(name = "nick_name", columnDefinition = "varchar(30)") 24 | private String nickName; 25 | 26 | @Column(name = "password", columnDefinition = "varchar(68)", nullable = false) 27 | private String password; 28 | 29 | @Column(name = "email", columnDefinition = "varchar(40)", nullable = false) 30 | private String email; 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Long id) { 37 | this.id = id; 38 | } 39 | 40 | public String getUserName() { 41 | return userName; 42 | } 43 | 44 | public void setUserName(String userName) { 45 | this.userName = userName; 46 | } 47 | 48 | public String getNickName() { 49 | return nickName; 50 | } 51 | 52 | public void setNickName(String nickName) { 53 | this.nickName = nickName; 54 | } 55 | 56 | public String getPassword() { 57 | return password; 58 | } 59 | 60 | public void setPassword(String password) { 61 | this.password = password; 62 | } 63 | 64 | public String getEmail() { 65 | return email; 66 | } 67 | 68 | public void setEmail(String email) { 69 | this.email = email; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/entity/UserRole.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.entity; 2 | 3 | import javax.persistence.*; 4 | 5 | /** 6 | * UserRole 7 | * 8 | * @author star 9 | */ 10 | @Entity 11 | @Table(name = "user_role") 12 | public class UserRole extends AbstractAuditingEntity { 13 | 14 | private static final long serialVersionUID = 1997955934111931587L; 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private Long id; 19 | 20 | @Column(name = "user_name", nullable = false) 21 | private String userName; 22 | 23 | @Column(name = "role", length = 15, nullable = false) 24 | private String role; 25 | 26 | public Long getId() { 27 | return id; 28 | } 29 | 30 | public void setId(Long id) { 31 | this.id = id; 32 | } 33 | 34 | public String getUserName() { 35 | return userName; 36 | } 37 | 38 | public void setUserName(String userName) { 39 | this.userName = userName; 40 | } 41 | 42 | public String getRole() { 43 | return role; 44 | } 45 | 46 | public void setRole(String role) { 47 | this.role = role; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/exception/AlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.exception; 2 | 3 | import org.zalando.problem.AbstractThrowableProblem; 4 | import org.zalando.problem.Status; 5 | import spring.security.jwt.constant.ErrorConstants; 6 | 7 | /** 8 | * AlreadyExistsException 9 | * 10 | * @author star 11 | */ 12 | public class AlreadyExistsException extends AbstractThrowableProblem { 13 | 14 | private static final long serialVersionUID = 4775907845387588528L; 15 | 16 | public AlreadyExistsException(String message) { 17 | super(ErrorConstants.DEFAULT_TYPE, message, Status.CONFLICT); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/exception/FieldError.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.exception; 2 | 3 | import java.io.Serializable; 4 | 5 | public class FieldError implements Serializable { 6 | 7 | private static final long serialVersionUID = 780120950461726941L; 8 | 9 | private final String objectName; 10 | 11 | private final String field; 12 | 13 | private final String message; 14 | 15 | public FieldError(String dto, String field, String message) { 16 | this.objectName = dto; 17 | this.field = field; 18 | this.message = message; 19 | } 20 | 21 | public String getObjectName() { 22 | return objectName; 23 | } 24 | 25 | public String getField() { 26 | return field; 27 | } 28 | 29 | public String getMessage() { 30 | return message; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.exception; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 6 | import org.springframework.validation.BindingResult; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.client.HttpStatusCodeException; 11 | import org.springframework.web.context.request.NativeWebRequest; 12 | import org.zalando.problem.DefaultProblem; 13 | import org.zalando.problem.Problem; 14 | import org.zalando.problem.ProblemBuilder; 15 | import org.zalando.problem.Status; 16 | import org.zalando.problem.spring.web.advice.ProblemHandling; 17 | import org.zalando.problem.spring.web.advice.validation.ConstraintViolationProblem; 18 | import spring.security.jwt.constant.ErrorConstants; 19 | 20 | import javax.annotation.Nonnull; 21 | import javax.annotation.Nullable; 22 | import javax.servlet.http.HttpServletRequest; 23 | import java.util.Date; 24 | import java.util.List; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * 全局异常处理器 29 | * 30 | *

31 | * 将服务器端异常处理为客户端友好的 JSON 格式。 32 | */ 33 | @ControllerAdvice 34 | public class GlobalExceptionHandler implements ProblemHandling { 35 | 36 | @Override 37 | public ResponseEntity process(@Nullable ResponseEntity entity, NativeWebRequest request) { 38 | if (entity == null || entity.getBody() == null) { 39 | return entity; 40 | } 41 | Problem problem = entity.getBody(); 42 | ProblemBuilder builder = Problem.builder() 43 | .withType(Problem.DEFAULT_TYPE.equals(problem.getType()) ? ErrorConstants.DEFAULT_TYPE : problem.getType()) 44 | .withStatus(problem.getStatus()) 45 | .withDetail(problem.getTitle()) 46 | .with("path", request.getNativeRequest(HttpServletRequest.class).getRequestURI()) 47 | .with("timestamp", new Date()); 48 | 49 | if (problem instanceof ConstraintViolationProblem) { 50 | builder 51 | .with("violations", ((ConstraintViolationProblem) problem).getViolations()) 52 | .with("message", ErrorConstants.ERR_VALIDATION); 53 | } else if (problem instanceof DefaultProblem) { 54 | builder 55 | .withCause(((DefaultProblem) problem).getCause()) 56 | .withDetail(problem.getDetail()) 57 | .withInstance(problem.getInstance()); 58 | problem.getParameters().forEach(builder::with); 59 | if (!problem.getParameters().containsKey("message") && problem.getStatus() != null) { 60 | builder.with("message", "error.http." + problem.getStatus().getStatusCode()); 61 | } 62 | } 63 | 64 | return new ResponseEntity<>(builder.build(), entity.getHeaders(), entity.getStatusCode()); 65 | } 66 | 67 | @Override 68 | public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, @Nonnull NativeWebRequest request) { 69 | BindingResult result = ex.getBindingResult(); 70 | List fieldErrors = result.getFieldErrors().stream() 71 | .map(f -> new FieldError(f.getObjectName(), f.getField(), f.getCode())) 72 | .collect(Collectors.toList()); 73 | 74 | Problem problem = Problem.builder() 75 | .withType(ErrorConstants.DEFAULT_TYPE) 76 | .withTitle("Method argument not valid") 77 | .withStatus(defaultConstraintViolationStatus()) 78 | .with("message", ErrorConstants.ERR_VALIDATION) 79 | .with("fieldErrors", fieldErrors) 80 | .build(); 81 | return create(ex, problem, request); 82 | } 83 | 84 | @ExceptionHandler(UsernameNotFoundException.class) 85 | public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException ex, NativeWebRequest request) { 86 | Problem problem = Problem.builder() 87 | .withStatus(Status.NOT_FOUND) 88 | .with("message", ex.getMessage()) 89 | .build(); 90 | return create(ex, problem, request); 91 | } 92 | 93 | @ExceptionHandler(ServiceException.class) 94 | public ResponseEntity handleServiceException(ServiceException ex, NativeWebRequest request) { 95 | Problem problem = Problem.builder() 96 | .withStatus(Status.INTERNAL_SERVER_ERROR) 97 | .withDetail(ex.getMessage()) 98 | .build(); 99 | return create(ex, problem, request); 100 | } 101 | 102 | @ExceptionHandler(HttpStatusCodeException.class) 103 | public ResponseEntity restTemplateException( 104 | NativeWebRequest request, HttpStatusCodeException ex) { 105 | // 尝试获取原始异常信息,通常这只对本项目的其它组件有效 106 | String responseBody = ex.getResponseBodyAsString(); 107 | return ResponseEntity 108 | .status(ex.getStatusCode()) 109 | .contentType(MediaType.APPLICATION_JSON) 110 | .body(responseBody); 111 | } 112 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/exception/ServiceException.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.exception; 2 | 3 | /** 4 | * ServiceException 5 | * 6 | * @author star 7 | */ 8 | public class ServiceException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = -8773139510494001347L; 11 | 12 | public ServiceException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/filter/JwtAuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.filter; 2 | 3 | import org.springframework.security.authentication.AuthenticationManager; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 7 | import org.springframework.util.StringUtils; 8 | import spring.security.jwt.constant.SecurityConstants; 9 | import spring.security.jwt.util.JwtUtils; 10 | 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import javax.validation.constraints.NotNull; 16 | import java.io.IOException; 17 | 18 | /** 19 | * JwtAuthorizationFilter 用户请求授权过滤器 20 | * 21 | *

22 | * 提供请求授权功能。用于处理所有 HTTP 请求,并检查是否存在带有正确 token 的 Authorization 标头。 23 | * 如果 token 有效,则过滤器会将身份验证数据添加到 Spring 的安全上下文中,并授权此次请求访问资源。

24 | * 25 | * @author star 26 | */ 27 | public class JwtAuthorizationFilter extends BasicAuthenticationFilter { 28 | 29 | public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { 30 | super(authenticationManager); 31 | } 32 | 33 | @Override 34 | protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { 35 | // 从 HTTP 请求中获取 token 36 | String token = this.getTokenFromHttpRequest(request); 37 | // 验证 token 是否有效 38 | if (StringUtils.hasText(token) && JwtUtils.validateToken(token)) { 39 | // 获取认证信息 40 | Authentication authentication = JwtUtils.getAuthentication(token); 41 | // 将认证信息存入 Spring 安全上下文中 42 | SecurityContextHolder.getContext().setAuthentication(authentication); 43 | } 44 | // 放行请求 45 | filterChain.doFilter(request, response); 46 | 47 | } 48 | 49 | /** 50 | * 从 HTTP 请求中获取 token 51 | * 52 | * @param request HTTP 请求 53 | * @return 返回 token 54 | */ 55 | private String getTokenFromHttpRequest(HttpServletRequest request) { 56 | String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER); 57 | if (authorization == null || !authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) { 58 | return null; 59 | } 60 | // 去掉 token 前缀 61 | return authorization.replace(SecurityConstants.TOKEN_PREFIX, ""); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/heartbeat/AbstractHeartBeatHandler.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.heartbeat; 2 | 3 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 | import spring.security.jwt.dto.HeartBeatRecordDTO; 5 | import spring.security.jwt.util.IpUtils; 6 | 7 | import java.time.Instant; 8 | import java.util.Objects; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.ThreadFactory; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * AbstractHeartBeatHandler 16 | * 17 | * @author star 18 | */ 19 | public abstract class AbstractHeartBeatHandler { 20 | 21 | private ScheduledExecutorService scheduledExecutorService; 22 | 23 | private HeartBeatLogger heartBeatLogger; 24 | 25 | private static final int threadNum = 1; 26 | 27 | protected abstract void handle(HeartBeatRecordDTO recordDTO); 28 | 29 | /** 30 | * 初始化 31 | */ 32 | public void init(HeartBeatConfig config, HeartBeatLogger logger) { 33 | this.heartBeatLogger = logger; 34 | // 初始化定时任务线程池 35 | ThreadFactory threadFactory = new ThreadFactoryBuilder() 36 | .setNameFormat("AbstractHeartBeatHandler-%s") 37 | .build(); 38 | scheduledExecutorService = Executors.newScheduledThreadPool(threadNum, threadFactory); 39 | scheduledExecutorService.scheduleWithFixedDelay(new HeartBeatTask(this.getHeartBeatRecord()), 40 | config.getDelayHandlerTime(), config.getIntervalTime(), 41 | TimeUnit.MILLISECONDS); 42 | } 43 | 44 | /** 45 | * 销毁定时任务线程池 46 | */ 47 | protected void destroy() { 48 | if (Objects.nonNull(scheduledExecutorService) && !scheduledExecutorService.isShutdown()) { 49 | scheduledExecutorService.shutdown(); 50 | scheduledExecutorService = null; 51 | } 52 | heartBeatLogger.info("心跳定时任务已关闭"); 53 | } 54 | 55 | private HeartBeatRecordDTO getHeartBeatRecord() { 56 | HeartBeatRecordDTO recordDTO = new HeartBeatRecordDTO(); 57 | try { 58 | recordDTO.setProjectPath(HeartBeatRecordHelper.getProjectPath()); 59 | recordDTO.setServerIp(IpUtils.getLocalIP()); 60 | recordDTO.setProcessNum(HeartBeatRecordHelper.getProcessId()); 61 | recordDTO.setProcessStartTime(HeartBeatRecordHelper.getProcessStartTime()); 62 | recordDTO.setHeartBeatTime(Instant.now()); 63 | } catch (Exception e) { 64 | heartBeatLogger.error("Get heart beat info error.", e); 65 | } 66 | return recordDTO; 67 | } 68 | 69 | private class HeartBeatTask implements Runnable { 70 | 71 | private final HeartBeatRecordDTO heartBeatRecord; 72 | 73 | public HeartBeatTask(HeartBeatRecordDTO heartBeatRecord) { 74 | this.heartBeatRecord = heartBeatRecord; 75 | } 76 | 77 | @Override 78 | public void run() { 79 | handle(this.heartBeatRecord); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/heartbeat/HeartBeatConfig.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.heartbeat; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | /** 7 | * HeartBeatConfig 8 | * 9 | * @author star 10 | */ 11 | @Configuration 12 | public class HeartBeatConfig { 13 | 14 | /** 15 | * 延迟执行时间 16 | */ 17 | @Value("${heart-beat.delayHandlerTime}") 18 | private Long delayHandlerTime; 19 | 20 | /** 21 | * 间隔执行时间 22 | */ 23 | @Value("${heart-beat.intervalTime}") 24 | private Long intervalTime; 25 | 26 | public Long getDelayHandlerTime() { 27 | return delayHandlerTime; 28 | } 29 | 30 | public void setDelayHandlerTime(Long delayHandlerTime) { 31 | this.delayHandlerTime = delayHandlerTime; 32 | } 33 | 34 | public Long getIntervalTime() { 35 | return intervalTime; 36 | } 37 | 38 | public void setIntervalTime(Long intervalTime) { 39 | this.intervalTime = intervalTime; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/heartbeat/HeartBeatHandler.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.heartbeat; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.BeanUtils; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.stereotype.Service; 9 | import spring.security.jwt.dto.HeartBeatRecordDTO; 10 | import spring.security.jwt.entity.HeartBeatRecord; 11 | import spring.security.jwt.repository.HeartBeatRecordRepository; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.annotation.PreDestroy; 15 | import java.time.Instant; 16 | import java.util.Optional; 17 | 18 | /** 19 | * HeartBeatHandler 20 | * 21 | * @author star 22 | */ 23 | @Component 24 | public class HeartBeatHandler extends AbstractHeartBeatHandler { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(HeartBeatHandler.class); 27 | 28 | @Autowired 29 | private HeartBeatRecordRepository heartBeatRecordRepository; 30 | 31 | @Autowired 32 | private HeartBeatConfig heartBeatConfig; 33 | 34 | @PostConstruct 35 | private void init() { 36 | super.init(heartBeatConfig, new HeartBeatLogger() { 37 | @Override 38 | public void error(String message) { 39 | log.error(message); 40 | } 41 | 42 | @Override 43 | public void error(String message, Throwable e) { 44 | log.error(message, e); 45 | } 46 | 47 | @Override 48 | public void info(String message) { 49 | log.info(message); 50 | } 51 | }); 52 | } 53 | 54 | @PreDestroy 55 | @Override 56 | public void destroy() { 57 | super.destroy(); 58 | } 59 | 60 | /** 61 | * 处理心跳记录 62 | * 63 | * @param recordDTO 心跳记录信息 64 | */ 65 | @Override 66 | public void handle(HeartBeatRecordDTO recordDTO) { 67 | Optional heartBeatRecordOptional = 68 | heartBeatRecordRepository.findByProjectPathAndServerIpAndProcessNum(recordDTO.getProjectPath(), 69 | recordDTO.getServerIp(), recordDTO.getProcessNum()); 70 | if (heartBeatRecordOptional.isPresent()) { 71 | HeartBeatRecord heartBeatRecord = heartBeatRecordOptional.get(); 72 | heartBeatRecord.setHeartBeatTime(Instant.now()); 73 | heartBeatRecordRepository.save(heartBeatRecord); 74 | return; 75 | } 76 | HeartBeatRecord heartBeatRecord = new HeartBeatRecord(); 77 | BeanUtils.copyProperties(recordDTO, heartBeatRecord); 78 | heartBeatRecordRepository.save(heartBeatRecord); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/heartbeat/HeartBeatLogger.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.heartbeat; 2 | 3 | /** 4 | * HeartBeatLogger 5 | */ 6 | public interface HeartBeatLogger { 7 | 8 | void error(String message); 9 | 10 | void error(String message, Throwable e); 11 | 12 | void info(String message); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/heartbeat/HeartBeatRecordHelper.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.heartbeat; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.lang.management.RuntimeMXBean; 5 | import java.time.Instant; 6 | 7 | /** 8 | * HeartBeatRecordHelper 9 | * 10 | * @author star 11 | */ 12 | public class HeartBeatRecordHelper { 13 | 14 | private HeartBeatRecordHelper() { 15 | 16 | } 17 | 18 | /** 19 | * 获取进程号 20 | * 21 | * @return 进程号 22 | */ 23 | public static Integer getProcessId() { 24 | RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 25 | String processName = runtimeMXBean.getName(); 26 | return Integer.valueOf(processName.split("@")[0]); 27 | 28 | } 29 | 30 | /** 31 | * 获取项目名称 32 | * 33 | * @return 项目路径 34 | */ 35 | public static String getProjectPath() { 36 | return System.getProperty("user.dir"); 37 | } 38 | 39 | /** 40 | * 获取进程启动时间 41 | * 42 | * @return 时间 43 | */ 44 | public static Instant getProcessStartTime() { 45 | RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 46 | return Instant.ofEpochMilli(runtimeMXBean.getStartTime()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/repository/HeartBeatRecordRepository.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.repository.query.Param; 5 | import spring.security.jwt.entity.HeartBeatRecord; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * HeartBeatRecordRepository 11 | * 12 | * @author star 13 | */ 14 | public interface HeartBeatRecordRepository extends JpaRepository { 15 | 16 | Optional findByProjectPathAndServerIpAndProcessNum(@Param("projectPath") String projectPath, 17 | @Param("serverIp") String serverIp, 18 | @Param("processNum") Integer processNum); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import spring.security.jwt.entity.User; 6 | 7 | import java.util.Optional; 8 | 9 | /** 10 | * UserRepository 11 | * 12 | * @author star 13 | */ 14 | public interface UserRepository extends JpaRepository { 15 | 16 | Optional findByUserName(String userName); 17 | 18 | @Modifying 19 | void deleteByUserName(String userName); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/repository/UserRoleRepository.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import spring.security.jwt.entity.UserRole; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * UserRoleRepository 11 | * 12 | * @author star 13 | */ 14 | public interface UserRoleRepository extends JpaRepository { 15 | 16 | List findByUserName(String userName); 17 | 18 | @Modifying 19 | void deleteByUserName(String userName); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/security/JwtUser.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.security; 2 | 3 | import spring.security.jwt.dto.UserDTO; 4 | 5 | /** 6 | * JwtUserDTO 7 | * 8 | * @author star 9 | */ 10 | public class JwtUser { 11 | 12 | private UserDTO user; 13 | 14 | private String token; 15 | 16 | public JwtUser(String token, UserDTO user) { 17 | this.user = user; 18 | this.token = token; 19 | } 20 | 21 | public UserDTO getUser() { 22 | return user; 23 | } 24 | 25 | public void setUser(UserDTO user) { 26 | this.user = user; 27 | } 28 | 29 | public String getToken() { 30 | return token; 31 | } 32 | 33 | public void setToken(String token) { 34 | this.token = token; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/security/config/JwtConfigurer.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.security.config; 2 | 3 | import spring.security.jwt.filter.JwtAuthorizationFilter; 4 | import org.springframework.security.config.annotation.SecurityConfigurerAdapter; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.web.DefaultSecurityFilterChain; 7 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 8 | 9 | /** 10 | * JwtConfigurer 11 | * 12 | * @author star 13 | */ 14 | public class JwtConfigurer extends SecurityConfigurerAdapter { 15 | 16 | private JwtAuthorizationFilter jwtAuthorizationFilter; 17 | 18 | public JwtConfigurer(JwtAuthorizationFilter jwtAuthorizationFilter) { 19 | this.jwtAuthorizationFilter = jwtAuthorizationFilter; 20 | } 21 | 22 | @Override 23 | public void configure(HttpSecurity http) { 24 | http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/security/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.security.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Import; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 12 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.config.http.SessionCreationPolicy; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | import org.springframework.security.web.authentication.HttpStatusEntryPoint; 17 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 18 | import org.springframework.web.filter.CorsFilter; 19 | import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; 20 | import spring.security.jwt.constant.SecurityConstants; 21 | import spring.security.jwt.filter.JwtAuthorizationFilter; 22 | 23 | /** 24 | * Web 安全配置 25 | * 26 | * @author star 27 | **/ 28 | @Configuration 29 | @EnableWebSecurity 30 | @EnableGlobalMethodSecurity(prePostEnabled = true) 31 | @Import(SecurityProblemSupport.class) 32 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 33 | 34 | @Autowired 35 | private CorsFilter corsFilter; 36 | 37 | @Autowired 38 | private SecurityProblemSupport securityProblemSupport; 39 | 40 | /** 41 | * 使用 Spring Security 推荐的加密方式进行登录密码的加密 42 | */ 43 | @Bean 44 | public BCryptPasswordEncoder bCryptPasswordEncoder(){ 45 | return new BCryptPasswordEncoder(); 46 | } 47 | 48 | /** 49 | * 此方法配置的资源路径不会进入 Spring Security 机制进行验证 50 | */ 51 | @Override 52 | public void configure(WebSecurity web) { 53 | web.ignoring() 54 | .antMatchers(HttpMethod.OPTIONS, "/**") 55 | .antMatchers("/app/**/*.{js,html}") 56 | .antMatchers("/v2/api-docs/**") 57 | .antMatchers("/i18n/**") 58 | .antMatchers("/test/**") 59 | .antMatchers("/h2") 60 | .antMatchers("/content/**") 61 | .antMatchers("/webjars/springfox-swagger-ui/**") 62 | .antMatchers("/swagger-resources/**") 63 | .antMatchers("/swagger-ui.html"); 64 | } 65 | 66 | /** 67 | * 定义安全策略,设置 HTTP 访问规则 68 | */ 69 | @Override 70 | protected void configure(HttpSecurity http) throws Exception { 71 | http 72 | .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) 73 | .exceptionHandling() 74 | // 当用户无权访问资源时发送 401 响应 75 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) 76 | // 当用户访问资源因权限不足时发送 403 响应 77 | .accessDeniedHandler(securityProblemSupport) 78 | .and() 79 | // 禁用 CSRF 80 | .csrf().disable() 81 | .headers().frameOptions().disable() 82 | .and() 83 | .logout().logoutUrl("/auth/logout").and() 84 | .authorizeRequests() 85 | // 指定路径下的资源需要进行验证后才能访问 86 | .antMatchers("/").permitAll() 87 | // 配置登录地址 88 | .antMatchers(HttpMethod.POST, SecurityConstants.AUTH_LOGIN_URL).permitAll() 89 | .antMatchers(HttpMethod.POST,"/api/users/register").permitAll() 90 | // 其他请求需验证 91 | .anyRequest().authenticated() 92 | .and() 93 | // 不需要 session(不创建会话) 94 | .sessionManagement() 95 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 96 | .and() 97 | .apply(securityConfigurationAdapter()); 98 | // super.configure(http); 99 | } 100 | 101 | private JwtConfigurer securityConfigurationAdapter() throws Exception{ 102 | return new JwtConfigurer(new JwtAuthorizationFilter(authenticationManager())); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/service/AuthService.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.authentication.BadCredentialsException; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.CollectionUtils; 11 | import spring.security.jwt.constant.UserRoleConstants; 12 | import spring.security.jwt.dto.UserDTO; 13 | import spring.security.jwt.dto.UserLoginDTO; 14 | import spring.security.jwt.entity.User; 15 | import spring.security.jwt.security.JwtUser; 16 | import spring.security.jwt.util.JwtUtils; 17 | 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | /** 23 | * 用户认证服务 24 | * 25 | * @author star 26 | */ 27 | @Service 28 | public class AuthService { 29 | 30 | @Autowired 31 | private UserService userService; 32 | 33 | @Autowired 34 | private BCryptPasswordEncoder bCryptPasswordEncoder; 35 | 36 | /** 37 | * 用户登录认证 38 | * 39 | * @param userLogin 用户登录信息 40 | */ 41 | public JwtUser authLogin(UserLoginDTO userLogin) { 42 | String userName = userLogin.getUserName(); 43 | String password = userLogin.getPassword(); 44 | 45 | // 根据登录名获取用户信息 46 | Optional userOptional = userService.getUserByName(userName); 47 | if (!userOptional.isPresent()) { 48 | throw new UsernameNotFoundException("User not found with userName: " + userName); 49 | } 50 | User user = userOptional.get(); 51 | // 验证登录密码是否正确。如果正确,则赋予用户相应权限并生成用户认证信息 52 | if (this.bCryptPasswordEncoder.matches(password, user.getPassword())) { 53 | List roles = userService.listUserRoles(userName); 54 | // 如果用户角色为空,则默认赋予 ROLE_USER 角色 55 | if (CollectionUtils.isEmpty(roles)) { 56 | roles = Collections.singletonList(UserRoleConstants.ROLE_USER); 57 | } 58 | // 生成 token 59 | String token = JwtUtils.generateToken(userName, roles, userLogin.getRememberMe()); 60 | 61 | // 认证成功后,设置认证信息到 Spring Security 上下文中 62 | Authentication authentication = JwtUtils.getAuthentication(token); 63 | SecurityContextHolder.getContext().setAuthentication(authentication); 64 | 65 | // 用户信息 66 | UserDTO userDTO = new UserDTO(); 67 | userDTO.setUserName(userName); 68 | userDTO.setEmail(user.getEmail()); 69 | userDTO.setRoles(roles); 70 | 71 | return new JwtUser(token, userDTO); 72 | } 73 | throw new BadCredentialsException("The user name or password error."); 74 | } 75 | 76 | /** 77 | * 用户退出登录 78 | * 79 | *

80 | * 清除 Spring Security 上下文中的认证信息 81 | */ 82 | public void logout() { 83 | SecurityContextHolder.clearContext(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/service/HeartBeatRecordService.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.service; 2 | 3 | import org.springframework.beans.BeanUtils; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.stereotype.Service; 8 | import spring.security.jwt.dto.HeartBeatRecordDTO; 9 | import spring.security.jwt.entity.HeartBeatRecord; 10 | import spring.security.jwt.repository.HeartBeatRecordRepository; 11 | 12 | /** 13 | * HeartBeatRecordService 14 | * 15 | * @author star 16 | */ 17 | @Service 18 | public class HeartBeatRecordService { 19 | 20 | @Autowired 21 | private HeartBeatRecordRepository heartBeatRecordRepository; 22 | 23 | public Page listByPage(Pageable pageable) { 24 | Page heartBeatRecordPage = heartBeatRecordRepository.findAll(pageable); 25 | return heartBeatRecordPage.map(heartBeatRecord -> { 26 | HeartBeatRecordDTO recordDTO = new HeartBeatRecordDTO(); 27 | BeanUtils.copyProperties(heartBeatRecord, recordDTO); 28 | return recordDTO; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/service/UserRoleService.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import spring.security.jwt.entity.UserRole; 7 | import spring.security.jwt.repository.UserRoleRepository; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * UserRoleService 13 | * 14 | * @author star 15 | */ 16 | @Service 17 | public class UserRoleService { 18 | 19 | @Autowired 20 | private UserRoleRepository userRoleRepository; 21 | 22 | public List listUserRoles(String userName) { 23 | return userRoleRepository.findByUserName(userName); 24 | } 25 | 26 | @Transactional(rollbackFor = Exception.class) 27 | public void deleteByUserName(String userName) { 28 | userRoleRepository.deleteByUserName(userName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/service/UserService.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.dao.DataIntegrityViolationException; 5 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import spring.security.jwt.dto.UserDTO; 10 | import spring.security.jwt.dto.UserRegisterDTO; 11 | import spring.security.jwt.entity.User; 12 | import spring.security.jwt.entity.UserRole; 13 | import spring.security.jwt.exception.AlreadyExistsException; 14 | import spring.security.jwt.repository.UserRepository; 15 | import spring.security.jwt.service.mapper.UserMapper; 16 | 17 | import java.util.List; 18 | import java.util.Optional; 19 | import java.util.stream.Collectors; 20 | 21 | /** 22 | * UserService 23 | * 24 | * @author star 25 | */ 26 | @Service 27 | public class UserService { 28 | 29 | @Autowired 30 | private UserMapper userMapper; 31 | 32 | @Autowired 33 | private UserRepository userRepository; 34 | 35 | @Autowired 36 | private BCryptPasswordEncoder bCryptPasswordEncoder; 37 | 38 | @Autowired 39 | private UserRoleService userRoleService; 40 | 41 | @Transactional(rollbackFor = Exception.class) 42 | public void register(UserRegisterDTO dto) { 43 | // 预检查用户名是否存在 44 | Optional userOptional = this.getUserByName(dto.getUserName()); 45 | if (userOptional.isPresent()) { 46 | throw new AlreadyExistsException("Save failed, the user name already exist."); 47 | } 48 | User user = userMapper.convertOfUserRegisterDTO(dto); 49 | // 将登录密码进行加密 50 | String cryptPassword = bCryptPasswordEncoder.encode(dto.getPassword()); 51 | user.setPassword(cryptPassword); 52 | try { 53 | userRepository.save(user); 54 | } catch (DataIntegrityViolationException e) { 55 | // 如果预检查没有检查到重复,就利用数据库的完整性检查 56 | throw new AlreadyExistsException("Save failed, the user name already exist."); 57 | 58 | } 59 | } 60 | 61 | public Optional getUserByName(String userName) { 62 | return userRepository.findByUserName(userName); 63 | 64 | } 65 | 66 | public UserDTO getUserInfoByName(String userName) { 67 | Optional userOptional = this.getUserByName(userName); 68 | if (!userOptional.isPresent()) { 69 | throw new UsernameNotFoundException("User not found with user name: " + userName); 70 | } 71 | // 获取用户角色 72 | List roles = this.listUserRoles(userName); 73 | User user = userOptional.get(); 74 | // 设置用户信息 75 | UserDTO userDTO = new UserDTO(); 76 | userDTO.setUserName(user.getUserName()); 77 | userDTO.setEmail(user.getEmail()); 78 | userDTO.setRoles(roles); 79 | 80 | return userDTO; 81 | } 82 | 83 | public List listUserRoles(String userName) { 84 | return userRoleService.listUserRoles(userName) 85 | .stream() 86 | .map(UserRole::getRole) 87 | .collect(Collectors.toList()); 88 | } 89 | 90 | @Transactional(rollbackFor = Exception.class) 91 | public void delete(String userName) { 92 | // 删除用户角色信息 93 | userRoleService.deleteByUserName(userName); 94 | // 删除用户基本信息 95 | userRepository.deleteByUserName(userName); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/service/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.service.mapper; 2 | 3 | import org.springframework.beans.BeanUtils; 4 | import org.springframework.stereotype.Service; 5 | import spring.security.jwt.dto.UserRegisterDTO; 6 | import spring.security.jwt.entity.User; 7 | 8 | /** 9 | * UserMapper 10 | * 11 | * @author star 12 | */ 13 | @Service 14 | public class UserMapper { 15 | 16 | public User convertOfUserRegisterDTO(UserRegisterDTO dto) { 17 | User user = new User(); 18 | BeanUtils.copyProperties(dto, user); 19 | 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/util/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yifanzheng/spring-security-jwt/896940412a1b0c084e32ed27bb5c0b82a606dbb6/src/main/java/spring/security/jwt/util/.DS_Store -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/util/IpUtils.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.net.InetAddress; 7 | import java.net.NetworkInterface; 8 | import java.util.ArrayList; 9 | import java.util.Enumeration; 10 | import java.util.List; 11 | import java.util.Objects; 12 | 13 | /** 14 | * ip 工具类 15 | * 16 | * @author star 17 | **/ 18 | public class IpUtils { 19 | 20 | public static final Logger log = LoggerFactory.getLogger(IpUtils.class); 21 | 22 | private IpUtils() { 23 | 24 | } 25 | 26 | /** 27 | * 获得服务器的IP地址 28 | */ 29 | public static String getLocalIP() { 30 | String ipStr = ""; 31 | InetAddress inetAddress = null; 32 | try { 33 | boolean isFindIp = false; 34 | Enumeration netInterfaces = NetworkInterface 35 | .getNetworkInterfaces(); 36 | while (netInterfaces.hasMoreElements()) { 37 | if (isFindIp) { 38 | break; 39 | } 40 | NetworkInterface ni = netInterfaces 41 | .nextElement(); 42 | Enumeration inetAddressList = ni.getInetAddresses(); 43 | while (inetAddressList.hasMoreElements()) { 44 | inetAddress = inetAddressList.nextElement(); 45 | if (!inetAddress.isLoopbackAddress() 46 | && inetAddress.getHostAddress().matches( 47 | "(\\d{1,3}\\.){3}\\d{1,3}")) { 48 | isFindIp = true; 49 | break; 50 | } 51 | } 52 | } 53 | } catch (Exception e) { 54 | log.error("Get ip address fialed, message: {}", e.getMessage()); 55 | } 56 | if (Objects.nonNull(inetAddress)) { 57 | ipStr = inetAddress.getHostAddress(); 58 | } 59 | return ipStr; 60 | } 61 | 62 | /** 63 | * 获得服务器的IP地址(多网卡) 64 | */ 65 | public static List getLocalIPS() { 66 | InetAddress ip = null; 67 | List ipList = new ArrayList(); 68 | try { 69 | Enumeration netInterfaces = NetworkInterface 70 | .getNetworkInterfaces(); 71 | while (netInterfaces.hasMoreElements()) { 72 | NetworkInterface ni = netInterfaces 73 | .nextElement(); 74 | Enumeration ips = ni.getInetAddresses(); 75 | while (ips.hasMoreElements()) { 76 | ip = ips.nextElement(); 77 | if (!ip.isLoopbackAddress() 78 | && ip.getHostAddress().matches( 79 | "(\\d{1,3}\\.){3}\\d{1,3}")) { 80 | ipList.add(ip.getHostAddress()); 81 | } 82 | } 83 | } 84 | } catch (Exception e) { 85 | log.error("Get ip address fialed, message: {}", e.getMessage()); 86 | } 87 | return ipList; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/util/JwtUtils.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.util; 2 | 3 | import io.jsonwebtoken.*; 4 | import io.jsonwebtoken.security.Keys; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 10 | import spring.security.jwt.constant.SecurityConstants; 11 | import spring.security.jwt.constant.UserRoleConstants; 12 | 13 | import javax.xml.bind.DatatypeConverter; 14 | import java.util.*; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * Jwt 工具类,用于生成、解析与验证 token 19 | * 20 | * @author star 21 | **/ 22 | public class JwtUtils { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); 25 | 26 | private static final byte[] secretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); 27 | 28 | private JwtUtils() { 29 | throw new IllegalStateException("Cannot create instance of static util class"); 30 | } 31 | 32 | /** 33 | * 根据用户名和用户角色生成 token 34 | * 35 | * @param userName 用户名 36 | * @param roles 用户角色 37 | * @param isRemember 是否记住我 38 | * @return 返回生成的 token 39 | */ 40 | public static String generateToken(String userName, List roles, boolean isRemember) { 41 | byte[] jwtSecretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); 42 | // 过期时间 43 | long expiration = isRemember ? SecurityConstants.EXPIRATION_REMEMBER_TIME : SecurityConstants.EXPIRATION_TIME; 44 | // 生成 token 45 | String token = Jwts.builder() 46 | // 生成签证信息 47 | .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE) 48 | .signWith(Keys.hmacShaKeyFor(jwtSecretKey), SignatureAlgorithm.HS256) 49 | .setSubject(userName) 50 | .claim(SecurityConstants.TOKEN_ROLE_CLAIM, roles) 51 | .setIssuer(SecurityConstants.TOKEN_ISSUER) 52 | .setIssuedAt(new Date()) 53 | .setAudience(SecurityConstants.TOKEN_AUDIENCE) 54 | // 设置有效时间 55 | .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) 56 | .compact(); 57 | return token; 58 | } 59 | 60 | /** 61 | * 验证 token 是否有效 62 | * 63 | *

64 | * 如果解析失败,说明 token 是无效的 65 | * 66 | * @param token token 信息 67 | * @return 如果返回 true,说明 token 有效 68 | */ 69 | public static boolean validateToken(String token) { 70 | try { 71 | getTokenBody(token); 72 | return true; 73 | } catch (ExpiredJwtException e) { 74 | logger.warn("Request to parse expired JWT : {} failed : {}", token, e.getMessage()); 75 | } catch (UnsupportedJwtException e) { 76 | logger.warn("Request to parse unsupported JWT : {} failed : {}", token, e.getMessage()); 77 | } catch (MalformedJwtException e) { 78 | logger.warn("Request to parse invalid JWT : {} failed : {}", token, e.getMessage()); 79 | } catch (IllegalArgumentException e) { 80 | logger.warn("Request to parse empty or null JWT : {} failed : {}", token, e.getMessage()); 81 | } 82 | return false; 83 | } 84 | 85 | /** 86 | * 根据 token 获取用户认证信息 87 | * 88 | * @param token token 信息 89 | * @return 返回用户认证信息 90 | */ 91 | public static Authentication getAuthentication(String token) { 92 | Claims claims = getTokenBody(token); 93 | // 获取用户角色字符串 94 | List roles = (List)claims.get(SecurityConstants.TOKEN_ROLE_CLAIM); 95 | List authorities = 96 | Objects.isNull(roles) ? Collections.singletonList(new SimpleGrantedAuthority(UserRoleConstants.ROLE_USER)) : 97 | roles.stream() 98 | .map(SimpleGrantedAuthority::new) 99 | .collect(Collectors.toList()); 100 | // 获取用户名 101 | String userName = claims.getSubject(); 102 | 103 | return new UsernamePasswordAuthenticationToken(userName, token, authorities); 104 | 105 | } 106 | 107 | private static Claims getTokenBody(String token) { 108 | return Jwts.parser() 109 | .setSigningKey(secretKey) 110 | .parseClaimsJws(token) 111 | .getBody(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/util/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.util; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContext; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * SecurityUtils 12 | * 13 | *

14 | * 用于获取当前登录的用户名 15 | * 16 | * @author star 17 | **/ 18 | public class SecurityUtils { 19 | 20 | private SecurityUtils() { 21 | throw new IllegalStateException("Cannot create instance of static util class"); 22 | } 23 | 24 | /** 25 | * 从上下文中获取当前登录的用户信息 26 | */ 27 | public static Optional getCurrentUserLogin() { 28 | // 获取上下文对象 29 | SecurityContext context = SecurityContextHolder.getContext(); 30 | // 获取验证信息 31 | Authentication authentication = context.getAuthentication(); 32 | // 返回上下文中的用户信息 33 | return Optional.ofNullable(authentication) 34 | .map(auth -> { 35 | if (auth.getPrincipal() instanceof UserDetails) { 36 | UserDetails userDetails = (UserDetails) auth.getPrincipal(); 37 | 38 | return userDetails.getUsername(); 39 | } else if (auth.getPrincipal() instanceof String) { 40 | return (String) auth.getPrincipal(); 41 | } 42 | return null; 43 | }); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/web/rest/AuthResource.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.web.rest; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import spring.security.jwt.constant.SecurityConstants; 14 | import spring.security.jwt.dto.UserDTO; 15 | import spring.security.jwt.dto.UserLoginDTO; 16 | import spring.security.jwt.security.JwtUser; 17 | import spring.security.jwt.service.AuthService; 18 | 19 | /** 20 | * AuthResource 21 | * 22 | * @author star 23 | */ 24 | @RestController 25 | @RequestMapping("/api/auth") 26 | @Api(tags = "Auth") 27 | public class AuthResource { 28 | 29 | @Autowired 30 | private AuthService authService; 31 | 32 | @PostMapping("/login") 33 | @ApiOperation(value = "用户登录认证") 34 | public ResponseEntity login(@RequestBody UserLoginDTO userLogin) { 35 | // 用户登录认证 36 | JwtUser jwtUser = authService.authLogin(userLogin); 37 | // 认证成功后,将 token 存入响应头中返回 38 | HttpHeaders httpHeaders = new HttpHeaders(); 39 | // 添加 token 前缀 "Bearer " 40 | httpHeaders.set(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + jwtUser.getToken()); 41 | 42 | return new ResponseEntity<>(jwtUser.getUser(), httpHeaders, HttpStatus.OK); 43 | 44 | } 45 | 46 | @PostMapping("/logout") 47 | @ApiOperation(value = "用户退出登录") 48 | public ResponseEntity logout() { 49 | authService.logout(); 50 | return ResponseEntity.ok().build(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/web/rest/HeartBeatResource.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.web.rest; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.web.PageableDefault; 8 | import org.springframework.data.web.SortDefault; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import spring.security.jwt.dto.HeartBeatRecordDTO; 15 | import spring.security.jwt.dto.PageDTO; 16 | import spring.security.jwt.heartbeat.HeartBeatHandler; 17 | import spring.security.jwt.service.HeartBeatRecordService; 18 | 19 | /** 20 | * @author star 21 | */ 22 | @RestController 23 | @RequestMapping("/api") 24 | public class HeartBeatResource { 25 | 26 | @Autowired 27 | private HeartBeatRecordService heartBeatRecordService; 28 | 29 | @GetMapping("/heartBeat/list") 30 | public ResponseEntity> listByPage(@PageableDefault(sort = {"heartBeatTime"}, 31 | direction = Sort.Direction.DESC) Pageable pageable) { 32 | Page page = heartBeatRecordService.listByPage(pageable); 33 | 34 | return ResponseEntity.ok(PageDTO.of(page.getTotalPages(), page.getContent())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/spring/security/jwt/web/rest/UserResource.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt.web.rest; 2 | 3 | import io.swagger.annotations.Api; 4 | import io.swagger.annotations.ApiOperation; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.web.bind.annotation.*; 9 | import spring.security.jwt.dto.UserDTO; 10 | import spring.security.jwt.dto.UserRegisterDTO; 11 | import spring.security.jwt.service.UserService; 12 | 13 | import javax.validation.Valid; 14 | 15 | /** 16 | * UserResource 17 | * 18 | * @author star 19 | */ 20 | @RestController 21 | @RequestMapping("/api/users") 22 | @Api(tags = {"用户资源"}) 23 | public class UserResource { 24 | 25 | @Autowired 26 | private UserService userService; 27 | 28 | @GetMapping("/{userName}") 29 | @ApiOperation(value = "根据用户名获取用户信息") 30 | public ResponseEntity getUser(@PathVariable String userName) { 31 | UserDTO userDTO = userService.getUserInfoByName(userName); 32 | return ResponseEntity.ok(userDTO); 33 | } 34 | 35 | @PostMapping("/register") 36 | @ApiOperation(value = "用户注册") 37 | public ResponseEntity register(@RequestBody @Valid UserRegisterDTO userRegister) { 38 | userService.register(userRegister); 39 | return ResponseEntity.ok().build(); 40 | } 41 | 42 | @DeleteMapping("/{userName}") 43 | @PreAuthorize("hasRole('ROLE_ADMIN')") 44 | @ApiOperation(value = "根据用户名删除用户信息") 45 | public ResponseEntity deleteByUserName(@PathVariable("userName") String userName) { 46 | userService.delete(userName); 47 | return ResponseEntity.ok().build(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://localhost:3307/jwt?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true 2 | spring.datasource.username=root 3 | spring.datasource.password=root 4 | spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver 5 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect 6 | 7 | #\u4EE5\u4E0B\u914D\u7F6E\uFF0C\u4E3B\u8981\u9632\u6B62\u9AD8\u7248\u672CSpringBoot\u6574\u5408Swagger\u542F\u52A8\u62A5\u9519Failed to start bean \u2018documentationPluginsBootstrapper\u2018 \u95EE\u9898 8 | spring.mvc.pathmatch.matching-strategy=ant_path_matcher 9 | 10 | ######################### heart beat ######################### 11 | heart-beat.delayHandlerTime=60000 12 | heart-beat.intervalTime=300000 -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | spring.profiles.active=dev 3 | 4 | ## \u6570\u636E\u6E90\u914D\u7F6E\u4FE1\u606F(\u6362\u6210\u81EA\u5DF1\u7684\u6570\u636E\u5E93) 5 | spring.datasource.url=jdbc:h2:mem:jwt 6 | spring.datasource.username=test 7 | spring.datasource.password=test 8 | spring.datasource.driverClassName=org.h2.Driver 9 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect 10 | spring.jpa.show-sql=false 11 | spring.jpa.hibernate.ddl-auto=update 12 | spring.jpa.open-in-view=false 13 | 14 | spring.datasource.platform=h2 15 | spring.h2.console.path=/h2 16 | spring.h2.console.enabled=true 17 | 18 | spring.datasource.type=com.zaxxer.hikari.HikariDataSource 19 | spring.datasource.hikari.minimumIdle=10 20 | spring.datasource.hikari.maximumPoolSize=30 21 | spring.datasource.hikari.idleTimeout=30000 22 | spring.datasource.hikari.connectionTimeout=30000 23 | spring.datasource.hikari.autoCommit=true 24 | spring.datasource.hikari.data-source-properties.cachePrepStmts=true 25 | spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250 26 | spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048 27 | spring.datasource.hikari.data-source-properties.sendStringParametersAsUnicode=false 28 | 29 | ## Jackson \u914D\u7F6E 30 | spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false 31 | spring.jackson.time-zone=GMT+8 -------------------------------------------------------------------------------- /src/test/java/spring/security/jwt/SpringSecurityApplicationTests.java: -------------------------------------------------------------------------------- 1 | package spring.security.jwt; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.junit4.SpringRunner; 8 | 9 | @RunWith(SpringRunner.class) 10 | @SpringBootTest 11 | public class SpringSecurityApplicationTests { 12 | 13 | @Test 14 | public void contextLoads() { 15 | } 16 | 17 | } 18 | --------------------------------------------------------------------------------