springboot 版本
security 版本
wagger 版本
jwt 版本
redis 版本 pom檔案如下
引入redis 是為了儲存 token
<version>3.3.5</version>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.8</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 如果jdk大於1.8,則還需匯入下面依賴-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!--jwt-->
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
yaml檔案的配置如下
server:
port: 8586 #埠號
servlet:
context-path: /api #介面統一自帶字首 (也就是因為這個配置很多地方需要改動)
spring:
application:
name: demoServe # 名字
datasource: #資料庫
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:xx/xx?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useUnicode=true
username: xxx #資料庫使用者
password: xxxx #密碼
data:
redis:
database: x #換成自己要用的庫 例 1
host: xx.xx.xx.xx #換成自己的地址
port: xx #自己的埠
password:
pool:
max-idle: 8
min-idle: 0
max-active: 8
max-wait: 8
timeout: 5000
springdoc:
api-docs:
path: /v3/api-docs
group: default
enabled: true
swagger-ui: #去掉 url 和 config-url 引數 可以正常訪問 加上後需要配置正確路徑 (暫時不理解)不然報404錯誤。
path: /swagger-ui.html
#base-url: /api
enabled: true
建立 swagger 的配置檔案 SwaggerConfig
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("demo介面文件")
.description("SpringBoot3 整合 Swagger3介面文件")
.version("v1"))
.externalDocs(new ExternalDocumentation()
.description("專案API文件")
.url("/"));
}
建立 security 的配置檔案 如下
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class NewWebSecurityConfig {
@Autowired
UserNameAuthenticationProvider userNameAuthenticationProvider; //是用security代理的登入
private final JwtAuthenticationEntryPoint unauthorizedHandler; //未授權處理邏輯
private final RestAuthenticationAccessDeniedHandler accessDeniedHandler; //許可權不足
private final JwtAuthenticationTokenFilter authenticationTokenFilter; //全域性訪問統一入口
public NewWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, RestAuthenticationAccessDeniedHandler accessDeniedHandler, JwtAuthenticationTokenFilter authenticationTokenFilter) {
this.unauthorizedHandler = unauthorizedHandler;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationTokenFilter = authenticationTokenFilter;
}
// 獲取AuthenticationManager(認證管理器),登入時認證使用
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/","login/token","/api/login.token","/login/token").permitAll() // 放行的介面
.requestMatchers("/api/swagger-ui/**", "/api/*/api-docs/**").permitAll() // 允許未登入使用者訪問 Swagger UI .anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(AbstractHttpConfigurer::disable) // 禁用
.cors(cors -> cors.disable()) // 使用具體的實現物件 (這裡我一直配不好 所以選擇了禁用)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(unauthorizedHandler)
.accessDeniedHandler(accessDeniedHandler)
)
.authenticationProvider(userNameAuthenticationProvider);
// 登入前驗證token
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(
"/webjars/**",
"/api/swagger-ui.html/**",
"/swagger-resources/**",
"/api/swagger-resources/**",
"/v2/**",
"/v3/**",
"/swagger-ui/**",
"/swagger/**"
);
}
/**
* 配置 CORS
* @return // 因為上邊配置一直 有問題 所以這裡也注掉了
*/
/* @Bean
public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("/**")); // 允許所有來源
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允許的方法
configuration.setAllowedHeaders(List.of("*")); // 允許的頭
configuration.setAllowCredentials(true); // 允許憑證
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }*/ /**
* 裝載BCrypt密碼編碼器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Utils.getMD5((String) rawPassword));
}
@Override
public String encode(CharSequence rawPassword) {
return MD5Utils.getMD5((String) rawPassword);
}
};
}
}
建立未授權處理邏輯檔案 JwtAuthenticationEntryPoint
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
//驗證為未登陸狀態會進入此方法,認證錯誤
//System.out.println("認證失敗:" + authException.getMessage());
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
/* JsonResultInfo<String> result=new JsonResultInfo<String>();
result.setCode("401"); result.setMsg(authException.getMessage());*/ ObjectMapper mapper =new ObjectMapper();
Map<String,Object> map =new HashMap<String, Object>();
map.put("code", "401");
map.put("msg", "使用者沒有登入,請登入");
//String body = ResultJson.failure(ResultCode.UNAUTHORIZED, authException.getMessage()).toString();
printWriter.write(mapper.writeValueAsString(map));
// printWriter.write(Gson.toJSONString(result));
printWriter.flush();
}
}
建立許可權不足檔案 RestAuthenticationAccessDeniedHandler
@Component("restAuthenticationAccessDeniedHandler")
public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 登陸狀態下,許可權不足執行該方法
// System.out.println("許可權不足:" + e.getMessage());
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 使用標準的403狀態碼
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = new HashMap<>();
map.put("code", "403");
map.put("msg", "沒有操作許可權");
printWriter.write(mapper.writeValueAsString(map));
printWriter.flush();
}
}
建立全域性訪問統一入口檔案JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
System.out.println(request.getRequestURL());
if(JwtUtils.vaild(token, Constants.LOGIN_SERCET) && SecurityContextHolder.getContext().getAuthentication() == null) {
Claims claims = JwtUtils.getClaims(token, Constants.LOGIN_SERCET);
String userId = (String)claims.get("userId");
//去redis 拿到token 進行對比
String token1 = (String) redisTemplate.opsForHash().get(Constants.TOKEN + ":" + userId+":", Constants.KEY); //這裡的常量可以自定義 與後邊的檔案對應上即可
String userStr = (String) redisTemplate.opsForHash().get(Constants.TOKEN + ":" + userId+":", Constants.USER );
if(token.equals(token1) && null !=userStr) {
ObjectMapper mapper =new ObjectMapper();
UserDetail userDetail = mapper.readValue(userStr, UserDetail.class);
redisTemplate.expire(Constants.TOKEN+":"+userId+":", 30, TimeUnit.MINUTES);
// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info(String.format("Authenticated userDetail %s, setting security context", userDetail.getUsername()));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
建立UserNameAuthenticationProvider類
@Slf4j
@Component
public class UserNameAuthenticationProvider implements AuthenticationProvider {
@Autowired
private LoginServiceImpl loginService;
@SneakyThrows
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException {
long time = System.currentTimeMillis();
log.info("使用者名稱/密碼 開始登入驗證 time:{}", time);
LoginDto params = (LoginDto) authentication.getPrincipal();
UserDetail userDetails =loginService.loadUserByUsername(params);
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
log.info("使用者名稱/密碼 登入驗證完成 time:{}, existTime:{}", time, (System.currentTimeMillis() - time));
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
建立 controller
@RestController
@RequestMapping("/login")
@Tag(name = "登入模組",description = "login")
public class LoginController {
@Autowired
private AuthenticationManager authenticationManager;
@Resource
private RedisTemplate<String,String> redisTemplate;
@Operation(summary = "登入")
@PostMapping("/token")
public JsonResultInfo postToken(@RequestBody LoginDto param) throws JsonProcessingException {
Authentication authentication = null;
UserDetail userDetail = null;
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(param, param.getPassWord()));
SecurityContextHolder.getContext().setAuthentication(authentication);
userDetail = (UserDetail) authentication.getPrincipal();
JsonResultInfo jsonResultInfo = new JsonResultInfo();
if (userDetail != null) { // 使用者不為空生成token 並儲存
Claims cliams = Jwts.claims();
cliams.put("userId", userDetail.getId());
cliams.put("time", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = new HashMap<String, Object>();
String token = JwtUtils.createToken(cliams, Constants.LOGIN_SERCET);
map.put("token", token);
map.put("userId", userDetail.getId());
// map.put("clientCode", client.getClientCode());
// redisTemplate.opsForValue().set(Constants.TOKEN+":"+userDetail.getId()+":"+type, mapper.writeValueAsString(userDetail), 1000, TimeUnit.MINUTES); redisTemplate.opsForHash().put(Constants.TOKEN + ":" + userDetail.getId() + ":" , Constants.USER, mapper.writeValueAsString(userDetail));
redisTemplate.opsForHash().put(Constants.TOKEN + ":" + userDetail.getId() + ":" , Constants.KEY, token);
redisTemplate.expire(Constants.TOKEN + ":" + userDetail.getId() + ":", 30, TimeUnit.MINUTES);
jsonResultInfo.setCode(CommonEnum.LOGIN_SUCCESS.getResultCode());
jsonResultInfo.setMsg(CommonEnum.LOGIN_SUCCESS.getResultMsg());
jsonResultInfo.setData(map);
return jsonResultInfo;
}
return jsonResultInfo;
}
}
整合完畢
訪問http://localhost:yourPort/api/swagger-ui/index.html#/
訪問 swagger
如果 swagger 配置錯誤會出現一些pet user 這些介面檢索自己的 swagger 配置。