1. 程式人生 > 其它 >SpringCloud整合SpringSecurity - JWT進行認證 ,鑑權

SpringCloud整合SpringSecurity - JWT進行認證 ,鑑權

一. 建立認證微服務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);
    }

}