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);
    }

}
posted @ 2021-12-02 19:31  清霜半落沙痕浅  阅读(1501)  评论(1编辑  收藏  举报