微信自建小程序seo排名优化怎样
背景
前一个项目基于springboot2做的后台服务,使用到了spring security做权限验证,token是用java生成的uuid,把token信息存储到了redis服务中。
新的项目计划使用springboot3,且希望使用JWT实现token,以下重新记录下实现思路。
项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.9</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.jy.bike</groupId><artifactId>bike</artifactId><version>0.0.1-SNAPSHOT</version><name>bike</name><description>Demo project for Spring Boot</description><url/><properties><mysql-connector>8.0.18</mysql-connector><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.10</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector}</version></dependency><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
Spring Security的鉴权原理
通过jwt生成token后,后续接口请求时,在header中传入jwt token,通过自定义JwtAuthenticationFilter获取登录用户信息,并放在spring security context里。由后续UsernamePasswordAuthenticationFilter验证和拦截鉴权
实现步骤
1.配置SecurityConfiguration
其中包括:白名单放行(swagger,login等资源),自定义JwtAuthenticationFilter并放在UsernamePasswordAuthenticationFilter之前,自定义UserService并通过其userDetailsService方法获取用户信息,使用BCryptPasswordEncoder密文验证账号密码,
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Autowiredprivate UserService userService;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 关闭跨站请求http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(request ->// 配置放行白名单request.requestMatchers("login", "logout").permitAll().requestMatchers("swagger-ui/*", "v3/api-docs", "v3/api-docs/*", "/druid/**").permitAll().anyRequest().authenticated())// 禁用session.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS)).authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();authProvider.setUserDetailsService(userService.userDetailsService());authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}
}
2.定义一个实体类,继承UserDetails类
用于放在spring security context里,里面包括登录账号的名称,密码,权限,状态等
public class LoginUserDetails implements UserDetails {private static final long serialVersionUID = 1L;private User user;public LoginUserDetails(User user) {this.user = user;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return List.of();}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getName();}@Overridepublic boolean isEnabled() {return user.isEnabled();}
}
3.实现一个UserService,通过其loadUserByUsername获取用户
实际应该用username查询数据库获取,这里写死一个用户,并传入经过BCryptPasswordEncoder加密后的密文(原文是123456)
@Service
public class UserService {public UserDetailsService userDetailsService() {return new UserDetailsService() {@Overridepublic UserDetails loadUserByUsername(String username) {User curUser = new User();curUser.setName("madixin"); curUser.setPassword("$2a$10$Yt3wAk1P1aZZsJKnjGbnQehJD8F80tLS.tsenpPTC1kMrMdbjvN7.");curUser.setEnabled(true);LoginUserDetails loginUser = new LoginUserDetails(curUser);return loginUser;}};}
}
4.实现一个jwtservice,用于把用户信息加密和解密成token
@Service
public class JwtService implements IJwtService {@Value("${token.signing.key}")private String jwtSigningKey;@Overridepublic String extractUserName(String token) {return extractClaim(token, Claims::getSubject);}@Overridepublic String generateToken(UserDetails userDetails) {return generateToken(new HashMap<>(), userDetails);}@Overridepublic boolean isTokenValid(String token, UserDetails userDetails) {final String userName = extractUserName(token);return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);}private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {final Claims claims = extractAllClaims(token);return claimsResolvers.apply(claims);}private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername()).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();}private boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}private Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}private Claims extractAllClaims(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}private Key getSigningKey() {byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey);return Keys.hmacShaKeyFor(keyBytes);}
}
5.实现自己的login接口,验证登录账号和密码是否正确,是否禁用,如果通过,则使用jwtservice生成token返回。
调用authenticationManager.authenticate时,会自动调用UserService的loadUserByUsername获取用户和校验密码
@RestController
public class LoginController {private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);@Autowiredprivate LoginService loginService;/*** 登录方法** @param loginDto 登录信息* @return 结果*/@PostMapping("/login")public ResponseResult<String> login(@RequestBody LoginDto loginDto) {try {// 返回JWT令牌return ResponseResult.success(loginService.login(loginDto.getPhone(), loginDto.getPassword()));} catch (BikeBaseException e) {LOGGER.error(e.getMessage());return ResponseResult.fail(e.getErrorCode());}}
}@Service
public class LoginService {private static final Logger LOGGER = LoggerFactory.getLogger(LoginService.class);@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtService jwtService;public String login(String username, String password) throws BikeBaseException {// 该方法会去调用UserDetailsService.loadUserByUsernameAuthentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));LoginUserDetails loginUser = (LoginUserDetails) authentication.getPrincipal();if (loginUser == null){throw new BikeBaseException(ErrorCode.ILLEGAL_AUTHENTICATE);// 认证失败}return jwtService.generateToken(loginUser);}public void logout() throws BikeBaseException {}
}
6.自实现JwtAuthenticationFilter(第一步已配置在UsernamePasswordAuthenticationFilter前),从header中获取token,如果验证通过,则把用户信息放在spring security context里。
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtService jwtService;@Autowiredprivate UserService userService;@Overrideprotected void doFilterInternal(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)throws ServletException, IOException {final String authHeader = request.getHeader("Authorization");final String jwt;final String userEmail;if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) {filterChain.doFilter(request, response);return;}jwt = authHeader.substring(7);userEmail = jwtService.extractUserName(jwt);if (StringUtils.isNotEmpty(userEmail)&& SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = userService.userDetailsService().loadUserByUsername(userEmail);if (jwtService.isTokenValid(jwt, userDetails)) {SecurityContext context = SecurityContextHolder.createEmptyContext();UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));context.setAuthentication(authToken);SecurityContextHolder.setContext(context);}}filterChain.doFilter(request, response);}
}
7.自定义权限异常返回
实现AuthenticationEntryPoint和AccessDeniedHandler,通过response写会统一的异常返回。
@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");ServletOutputStream outputStream = response.getOutputStream();ObjectMapper objectMapper = new ObjectMapper();outputStream.write(objectMapper.writeValueAsString(ResponseResult.fail(ErrorCode.ILLEGAL_AUTHENTICATE)).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {response.setContentType("application/json;charset=utf-8");ServletOutputStream outputStream = response.getOutputStream();ObjectMapper objectMapper = new ObjectMapper();outputStream.write(objectMapper.writeValueAsString(ResponseResult.fail(ErrorCode.ILLEGAL_AUTHENTICATE)).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
在SecurityConfiguration中配置异常处理
http.exceptionHandling(configurer -> {configurer.authenticationEntryPoint(restAuthenticationEntryPoint);configurer.accessDeniedHandler(customerAccessDeniedHandler);
});
8.基于角色,更细粒度的控制每个接口的权限
通过在每个接口,配置@PreAuthorize装饰器实现,如
@PreAuthorize("@ss.hasPermi('sysadmin,company_admin,project_admin,worker')")
具体代码就不附上了。
参考
源码:https://github.com/buingoctruong/springboot3-springsecurity6-jwt
视频:2024最新SpringSecurity6安全框架教程-Spring Security+JWT实现项目级前端分离认证_哔哩哔哩_bilibili