SpringCloud整合SpringSecurity - JWT進行認證 ,鑑權
阿新 • • 發佈:2021-12-02
一. 建立認證微服務AuthenticationService
1.1 pom.xml
點選檢視程式碼
<dependencies> <!--mysql驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--mybatis-dynamic-sql , 可以使用mybatis plus或者自己寫sql--> <dependency> <groupId>org.mybatis.dynamic-sql</groupId> <artifactId>mybatis-dynamic-sql</artifactId> <version>1.2.1</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- jwt --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> </dependency> <!-- ssm spring boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring cloud --> <!-- spring cloud alibba --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
1.2 建立SimpleUserDetailsService 實現 UserDetailsService介面
作用:將資料查到的使用者資訊和許可權放進UserDetails物件,用於SpringSecurity進行認證
@Component @Slf4j public class SimpleUserDetailsService implements UserDetailsService { @Autowired private UserService userService; private final PasswordEncoder passwordEncoder; public SimpleUserDetailsService(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //從資料庫獲取使用者資訊 UserEntity userEntity = userService.getByUsername(s); //從資料庫獲取使用者的角色許可權 List<UserRoles> userRolesList = userService.getRolesByUsername(s); StringBuilder authorityBuilder = new StringBuilder(); //將角色資訊放進StringBuilder中 userRolesList.forEach(r-> { authorityBuilder.append("ROLE_").append(r.getRoleName().toUpperCase()).append(","); //從資料庫獲取使用者資源許可權 List<RolePermissions> permissionsList = userService.getPermissionsByRole(r.getRoleName()); permissionsList.forEach(p->authorityBuilder.append(p.getPermission()).append(",")); }); //獲取使用者密碼 String password = userEntity.getPassword(); log.info("password->"+password); log.info("authorities->"+authorityBuilder.toString()); //返回UserDetails物件 return new User(s,passwordEncoder.encode(password), AuthorityUtils.commaSeparatedStringToAuthorityList(authorityBuilder.toString())); } }
1.3 建立JWTAuthenticationSuccessHandler 實現 AuthenticationSuccessHandler介面
作用:自定義登入成功請求返回的結果,SpringSecurity預設登入成功後跳轉到登入前url或者"/",前後端分離專案需要登入成功後返回jwt-token
@Component public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler { //json private final ObjectMapper objectMapper = new ObjectMapper(); //操作redis private final HashOperations<String, String, String> operations; // 構造注入 public JWTAuthenticationSuccessHandler(RedisTemplate<String, String> redisTemplate) { this.operations = redisTemplate.opsForHash(); } @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { // authentication 物件攜帶了當前登陸使用者名稱等相關資訊 User user = (User) authentication.getPrincipal(); resp.setContentType("application/json;charset=UTF-8"); try { StringBuffer buffer = new StringBuffer(); user.getAuthorities().forEach(item -> { buffer.append(item.getAuthority()); buffer.append(","); }); buffer.deleteCharAt(buffer.length()-1); // 使用者的 username 和他所具有的許可權存入 redis 中。 operations.put(JWTUtil.REDIS_HASH_KEY, user.getUsername(), buffer.toString()); // 在 jwt-token-string 的荷載(payload)中存上當前使用者的名字. String jwtStr = JWTUtil.createJWT(user.getUsername()); Map<String, String> map = new HashMap<>(); map.put("code", "10000"); map.put("msg", "success"); map.put("jwt-token", jwtStr); PrintWriter out = resp.getWriter(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } } }
1.4 配置SpringSecurity,將JWTAuthenticationSuccessHandler的返回結果替換預設返回結果
@EnableWebSecurity(debug = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private SimpleUserDetailsService userDetailsService;
@Resource
private JWTAuthenticationSuccessHandler successHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 這是一個空的、假的密碼加密器。在加密時啥事沒幹。
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//任何請求都需要認證
http.authorizeRequests().anyRequest().authenticated();
//登入框登入,登入成功後使用自定義的JWTAuthenticationSuccessHandler
http.formLogin().successHandler(successHandler);
//禁用跨域過濾器
http.csrf().disable();
//禁用session過濾器
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
1.5 將服務註冊進nacos
- 啟動類上新增@EnableDiscoveryClient註解
@SpringBootApplication @EnableDiscoveryClient public class AuthenticationServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthenticationServiceApplication.class, args); } }
- 配置bootstrap.yml
點選檢視程式碼
spring: cloud: nacos: discovery: server-addr: 192.172.0.24:8848 password: nacos username: nacos group: Dracarys config: contextPath: /nacos server-addr: ${spring.cloud.nacos.discovery.server-addr} username: ${spring.cloud.nacos.discovery.username} password: ${spring.cloud.nacos.discovery.password} group: ${spring.cloud.nacos.discovery.group}
- 配置application.yml
點選檢視程式碼
server: port: 8080 spring: application: name: authentication-service datasource: driver-class-name: com.mysql.cj.jdbc.Driver password: 123root456 url: jdbc:mysql://114.55.6.86:3306/security_db?serverTimezone=UTC username: root redis: host: 114.55.6.86 port: 6379 password: 123
二. 建立普通需要鑑權的微服務SecurityService
2.1 pom.xml
點選檢視程式碼
<dependencies>
<!--jwt-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2 自定義JwtFilter過濾器,攔截請求,新增UseranmePasswordAuthenticationToken
@Slf4j
@Component
public class JwtFilter extends OncePerRequestFilter {
//操作redis
private final HashOperations<String, String, String> operations;
//構造注入
public JwtFilter(RedisTemplate<String, String> redisTemplate) {
this.operations = redisTemplate.opsForHash();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//如果security_context_holder中有authentication
if(authentication != null){
log.info("security_context_holder中有authentication");
filterChain.doFilter(request,response);
return;
}
// String jwtStr = request.getHeader("x-jwt-token");
String username = request.getHeader("x-username");
//如果請求頭裡沒有token
if(StringUtils.isEmpty(username)){
log.info("沒有username");
filterChain.doFilter(request,response);
return;
}
//jwtStr驗證不通過
/*if(!JwtUtils.verify(jwtStr)){
log.info("jwtStr驗證不通過");
filterChain.doFilter(request,response);
return;
}
String username = JwtUtils.getUsernameFromJWT(jwtStr);*/
//根據使用者名稱從redis中獲取許可權
log.info("username->"+username);
String authorities = operations.get("jwt-token",username);
log.info("authorities->"+operations.get("jwt-token",username));
//將許可權存進token中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
2.3 配置SpringSecurity,將自定義的JwtFilter過濾器新增進SpringSecurity過濾器鏈
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Resource
private JwtFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
//將自定義的Jwt過濾器新增到UsernamePasswordAuthenticationFilter後面
.formLogin().and().addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
2.4 將服務註冊到Nacos
- 啟動類上新增@EnableDiscoveryClient註解
@SpringBootApplication @EnableDiscoveryClient public class SecurityServiceDemo1Application { public static void main(String[] args) { SpringApplication.run(SecurityServiceDemo1Application.class, args); } }
- application.yml
點選檢視程式碼
#日誌 logging: level: root: INFO com.wn: DEBUG pattern: console: "${CONSOLE_LOG_PATTERN:%clr(${LOG_LEVEL_PATTERN:%5p}) %clr(|){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}" server: port: 9000 spring: application: name: security-service-demo1 cloud: nacos: discovery: group: Dracarys namespace: public password: nacos server-addr: 192.172.0.24:8848 username: nacos redis: port: 6379 host: 114.55.6.86
三. 建立Gateway微服務
3.1 pom.xml
點選檢視程式碼
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 自定義全域性過濾器GatewayFilter,將jwt-token解析,返回使用者名稱到請求頭
@Component
@Slf4j
public class GatewayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpHeaders headers = exchange.getRequest().getHeaders();
List<String> strings = headers.get("x-jwt-token");
ServerHttpRequest mutateRequest = exchange.getRequest();
//校驗token,成功拿到token中的username
if(strings.size()>0 && JWTUtil.verify(strings.get(0))){
String username = JWTUtil.getUsernameFromJWT(strings.get(0));
//將username存進請求頭
mutateRequest = exchange.getRequest().mutate().header("x-username", username).build();
log.info("將使用者名稱存進請求頭"+username);
}
return chain.filter(exchange.mutate().request(mutateRequest).build());
}
}
3.2 將服務註冊到nacos
- 啟動類上新增@EnableDiscoveryClient註解
- application.yml
點選檢視程式碼
#日誌 logging: level: root: INFO com.wn: DEBUG pattern: console: "${CONSOLE_LOG_PATTERN:%clr(${LOG_LEVEL_PATTERN:%5p}) %clr(|){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}" server: port: 88 spring: application: name: gateway cloud: nacos: discovery: server-addr: 192.172.0.24:8848 username: nacos password: nacos group: Dracarys #整合gateway和openFeign gateway: discovery: locator: enabled: true lower-case-service-id: true
四. JwtUtils
點選檢視程式碼
package com.wn.service.util;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.shaded.json.JSONObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Map;
@Slf4j
public class JwtUtils {
private static final String usernameKey = "username";
private static final String authoritiesKey = "authorities";
public static final String secret = "hello world goodbye thank you very much see you next time";
static {
log.info("spring security jwt secret: {}", secret);
}
@SneakyThrows
public static String createJWT(String username) {
// jwt 頭
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT).build();
// jwt 荷載
JSONObject obj = new JSONObject();
obj.put(usernameKey, username);
Payload payload = new Payload(obj);
// jwt 頭 + 荷載 + 金鑰 = 簽名
JWSSigner jwsSigner = new MACSigner(secret);
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 進行簽名(根據前兩部分生成第三部分)
jwsObject.sign(jwsSigner);
// 獲得 jwt string
return jwsObject.serialize();
}
@SneakyThrows
public static String createJWT(String username, Collection<? extends GrantedAuthority> authorities) {
// jwt 頭
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT).build();
// jwt 荷載
JSONObject obj = new JSONObject();
obj.put(usernameKey, username);
obj.put(authoritiesKey, StringUtils.collectionToCommaDelimitedString(authorities)); // "xxx,yyy,zzz,..."
Payload payload = new Payload(obj);
// jwt 頭 + 荷載 + 金鑰 = 簽名
JWSSigner jwsSigner = new MACSigner(secret);
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 進行簽名(根據前兩部分生成第三部分)
jwsObject.sign(jwsSigner);
// 獲得 jwt string
return jwsObject.serialize();
}
@SneakyThrows
public static boolean verify(String jwtString) {
JWSObject jwsObject = JWSObject.parse(jwtString);
JWSVerifier jwsVerifier = new MACVerifier(secret);
return jwsObject.verify(jwsVerifier);
}
@SneakyThrows
public static String getUsernameFromJWT(String jwtString) {
JWSObject jwsObject = JWSObject.parse(jwtString);
Map<String, Object> map = jwsObject.getPayload().toJSONObject();
return (String) map.get(usernameKey);
}
@SneakyThrows
public static String getAuthoritiesFromJwt(String jwtString) {
JWSObject jwsObject = JWSObject.parse(jwtString);
Map<String, Object> map = jwsObject.getPayload().toJSONObject();
return (String) map.get(authoritiesKey);
}
}