├── .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 | 
42 |
43 | **数据表设计**
44 |
45 | 用户表与用户角色表是多对多的关系。因为这里比较简单,所以表设计上有点冗余。小伙伴们可以根据实际情况重新设计。
46 | 
47 |
48 | **数据交互**
49 |
50 | 用户登录 -> 后端验证登录并返回 token -> 前端携带 token 请求后端数据 -> 后端返回数据。
51 |
52 | 
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 | 
378 | **登录**
379 | 
380 | **带上正确的 token 访问需要身份验证的资源**
381 | 
382 | **带上不正确的 token 访问需要身份验证的资源**
383 | 
384 |
385 | **不带 token 访问需要身份验证的资源**
386 | 
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 |
--------------------------------------------------------------------------------