SpringSecurity整合SpringBoot分布式版

分布式认证概念说明

分布式认证,即我们常说的单点登录,简称SSO,指的是在多应用系统的项目中,用户只需要登录一次,就可以访
问所有互相信任的应用系统。

分布式认证流程图

首先,我们要明确,在分布式项目中,每台服务器都有各自独立的session,而这些session之间是无法直接共享资
源的,所以,session通常不能被作为单点登录的技术方案。
最合理的单点登录方案流程如下图所示:

单点登录的实现分两大环节:

  • 用户认证:这一环节主要是用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token,
    主要在认证服务器中完成,即图中的A系统,注意A系统只能有一个。
  • 身份校验:这一环节是用户携带token去访问其他服务器时,在其他服务器中要对token的真伪进行检验,主
    要在资源服务器中完成,即图中的B系统,这里B系统可以有很多个。

JWT介绍

概念说明

从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的
健壮性,这里我们选择使用JWT来实现token的生成和校验。
JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可
以解析检验token。

JWT生成的token由三部分组成:

  • 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
  • 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
  • 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编
    码,就得到了签名。

JWT生成token的安全性分析

从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其
中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐
上面了!
试想:如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术
公开了?大家可以用这个盐来解析token,就能用来伪造token。
这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全
效果!

非对称加密RSA介绍

  • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
    • 私钥加密,持有私钥或公钥才可以解密
    • 公钥加密,持有私钥才可解密
  • 优点:安全,难以破解
  • 缺点:算法比较耗时,为了安全,可以接受
  • 历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三
    个人的名字缩写:RSA。

JWT相关工具类

依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>

载荷对象

/**
 * @author 黑马程序员 
 * 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象 
 */
@Data
public class Payload<T> {
    private String id;
    private T userInfo;
    private Date expiration;
}

JWT工具类

/**
 * @author: 黑马程序员
 * 生成token以及校验token相关方法
 */
public class JwtUtils {
    private static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 私钥加密token 
     *
     * @param userInfo 载荷中的数据 
     * @param privateKey 私钥 
     * @param expire 过期时间,单位分钟 
     * @return JWT 
     */
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder().claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo)).setId(createJTI()).setExpiration(DateTime.now().plusMinutes(expire).toDate()).signWith(privateKey, SignatureAlgorithm.RS256).compact();
    }

    /**
     * 私钥加密token 
     *
     * @param userInfo 载荷中的数据 
     * @param privateKey 私钥 
     * @param expire 过期时间,单位秒 
     * @return JWT 
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder().claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo)).setId(createJTI()).setExpiration(DateTime.now().plusSeconds(expire).toDate()).signWith(privateKey, SignatureAlgorithm.RS256).compact();
    }

    /**
     * 公钥解析token 
     *
     * @param token 用户请求中的token 
     * @param publicKey 公钥 
     * @return Jws<Claims> 
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /**
     * 获取token中的用户信息 
     *
     * @param token 用户请求中的令牌 
     * @param publicKey 公钥 
     * @return 用户信息 
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息 
     *
     * @param token 用户请求中的令牌 
     * @param publicKey 公钥 
     * @return 用户信息 
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }
}

RSA工具类

/**
 * @author 黑马程序员 
 */
public class RsaUtils {
    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中读取公钥 
     *
     * @param filename 公钥保存路径,相对于classpath 
     * @return 公钥对象 
     * @throws Exception 
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥 
     *
     * @param filename 私钥保存路径,相对于classpath 
     * @return 私钥对象 
     * @throws Exception 
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥 
     *
     * @param bytes 公钥的字节形式 
     * @return 
     * @throws Exception 
     */
    private static PublicKey getPublicKey(byte[] bytes) throws Exception {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥 
     *
     * @param bytes 私钥的字节形式 
     * @return 
     * @throws Exception 
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件 
     *
     * @param publicKeyFilename 公钥文件路径 
     * @param privateKeyFilename 私钥文件路径 
     * @param secret 生成密钥的密文 
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出 
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

SpringSecurity+JWT+RSA分布式认证思路分析

SpringSecurity主要是通过过滤器来实现功能的!我们要找到SpringSecurity实现认证和校验身份的过滤器!

回顾集中式认证流程

  • 用户认证:
    使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤
    器父类中successfulAuthentication方法实现认证成功后的操作。
  • 身份校验:
    使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。

分析分布式认证流程

  • 用户认证:
    由于,分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修
    改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。
    另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我
    们需要修改这个方法,在认证通过后生成token并返回给用户。
  • 身份校验:
    原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用
    户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于
    后续的授权功能可以正常使用。

SpringSecurity+JWT+RSA分布式认证实现

创建父工程并导入jar包

通用模块

创建通用子模块并导入JWT相关jar包

<dependencies>
    <!--jwt所需jar包-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.10.7</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.10.7</version>
        <scope>runtime</scope>
    </dependency>
    <!--lombok插件-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency> <!--处理日期工具包-->
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
    </dependency> <!--处理json工具包-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.9</version>
    </dependency> <!--日志包-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency> <!--测试包-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

导入工具类


相应的类在上面已经给出

在通用子模块中编写测试类生成rsa公钥和私钥

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class RsaUtilsTest {

    private String privateFilePath = "F:\\java\\code\\auth_key\\id_key_rsa";
    private String publicFilePath = "F:\\java\\code\\auth_key\\id_key_rsa.pub";


    @Test
    void getPublicKey() throws Exception {
        System.out.println(RsaUtils.getPublicKey(publicFilePath));
    }

    @Test
    void getPrivateKey() throws Exception {
        System.out.println(RsaUtils.getPrivateKey(privateFilePath));
    }

    @Test
    void generateKey() throws Exception {
        RsaUtils.generateKey(publicFilePath, privateFilePath, "ericjin", 2048);
    }
}

认证服务

创建认证服务工程并导入依赖

<dependencies>
    <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>com.ericjin</groupId>
        <artifactId>ericjin_common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

创建认证服务配置文件

server:
  port: 9001
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///security_authority
    username: root
    password: root
mybatis:
  type-aliases-package: com.ericjin.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.ericjin: debug
rsa:
  key:
    pubKeyPath: D:\\auth_key\\rsa_key.pub
    priKeyPath: D:\\auth_key\\rsa_key

提供解析公钥和私钥的配置类

import com.ericjin.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;

@Data
@ConfigurationProperties(prefix = "rsa.key")
public class RsaKeyProperties {
    private String pubKeyPath;
    private String priKeyPath;
    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void loadKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyPath);
        privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }
}

创建认证服务启动类

import com.ericjin.config.RsaKeyProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@MapperScan("com.ericjin.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)   // 将RsaKeyProperties放入到IOC容器中
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }
}

将上面集中式案例中数据库认证相关代码复制到认证服务中


注意这里要去掉mapper中继承的通用mapper接口
处理器类上换成@RestController,这里前后端绝对分离,不能再跳转页面了,要返回数据。

编写认证过滤器

import com.ericjin.config.RsaKeyProperties;
import com.ericjin.domain.SysRole;
import com.ericjin.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ericjin.domain.SysUser;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties prop;

    public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
        this.authenticationManager = authenticationManager;
        this.prop = prop;
    }

    /**
     * 将前端传过来的用户信息提取出来并传递下去
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            // 获取请求体中传来的数据
            SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sysUser.getUsername(),
                    sysUser.getPassword());
            return authenticationManager.authenticate(token);
        } catch (Exception e) {
            try {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter writer = response.getWriter();
                Map<String, Object> resultMap = new HashMap<>();
                resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                resultMap.put("msg", "用户名或密码错误");
                writer.write(new ObjectMapper().writeValueAsString(resultMap));
                writer.flush();
                writer.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }

    /**
     * 认证成功后回写token
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SysUser user = new SysUser();
        user.setUsername(authResult.getName());
        user.setRoles((List<SysRole>) authResult.getAuthorities());
        // 生成token
        String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
        // 将token写入请求头中
        response.addHeader("Authorization", "Bearer " + token);
        try {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            PrintWriter writer = response.getWriter();
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("code", HttpServletResponse.SC_OK);
            resultMap.put("msg", "认证通过");
            writer.write(new ObjectMapper().writeValueAsString(resultMap));
            writer.flush();
            writer.close();
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

编写检验token过滤器

import com.ericjin.config.RsaKeyProperties;
import com.ericjin.domain.Payload;
import com.ericjin.domain.SysUser;
import com.ericjin.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

public class JwtVerifyFilter extends BasicAuthenticationFilter {

    private RsaKeyProperties prop;

    public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
        super(authenticationManager);
        this.prop = prop;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header != null && !header.startsWith("Bearer ")) {
            // 如果携带了错误的token 给用户提示请登录
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter writer = response.getWriter();
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
            resultMap.put("msg", "请登录!");
            writer.write(new ObjectMapper().writeValueAsString(resultMap));
            writer.flush();
            writer.close();
        } else {
            // 如果携带了正确格式的token
            String token = header.replace("Bearer ", "");
            // 验证token是否正确
            // 先获取token中的载荷部分
            Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), SysUser.class);
            SysUser user = payload.getUserInfo();
            if (user != null) {
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
                // 将用户信息存入session,后面权限操作需要用到
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            }
        }
    }
}

编写SpringSecurity配置类

import com.ericjin.filter.JwtLoginFilter;
import com.ericjin.filter.JwtVerifyFilter;
import com.ericjin.springboot_security_jsp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Autowired
    private RsaKeyProperties prop;

    /**
     * 把加密对象放入IOC容器中
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 认证用户的来源【内存或者数据库】
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }


    // 配置SpringSecurity相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 释放静态资源 指定资源拦截规则 指定自定义认证页面 指定退出认证配置 csrf配置
        http.csrf().disable()  // 禁用csrf
                .authorizeRequests()
                .antMatchers("/product").hasAnyRole("USER", "ADMIN")   // 表示访问所有的页面都要USER或者ADMIN角色
                .anyRequest()
                .authenticated()    // 表明其他的资源只有通过认证后才能访问
                .and()          // 表示一个新的配置开始
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)      // 是否清空session
                .permitAll()
                .and()
                .addFilter(new JwtLoginFilter(super.authenticationManager(), prop))
                .addFilter(new JwtVerifyFilter(super.authenticationManager(), prop))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);   // 禁用session 不再使用
    }
}

启动测试认证服务



认证通过

token信息也已经存在,搭建成功!

资源服务

说明

资源服务可以有很多个,这里只拿产品服务为例,记住,资源服务中只能通过公钥验证认证。不能签发token!

创建产品服务并导入依赖

根据实际业务导包即可

<dependencies>
    <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>com.ericjin</groupId>
        <artifactId>ericjin_common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot</artifactId>
    </dependency>
</dependencies>

编写产品服务配置文件

切记这里只能有公钥地址!

server:
  port: 9002
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///security_authority
    username: root
    password: root
mybatis:
  type-aliases-package: com.ericjin.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.ericjin: debug
rsa:
  key:
    pubKeyPath: F:\\java\\code\\auth_key\\id_key_rsa.pub

编写读取公钥的配置类

import com.ericjin.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.annotation.PostConstruct;
import java.security.PublicKey;

@Data
@ConfigurationProperties(prefix = "rsa.key")
public class RsaKeyProperties {
    private String pubKeyPath;
    private PublicKey publicKey;

    @PostConstruct
    public void loadKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyPath);
    }
}

编写启动类

import com.ericjin.config.RsaKeyProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@MapperScan("com.ericjin.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)   // 将RsaKeyProperties放入到IOC容器中
public class SourceProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(SourceProductApplication.class, args);
    }
}

复制认证服务中,用户对象,角色对象和校验认证的接口

复制认证服务中SpringSecurity配置类做修改

去掉“增加自定义认证过滤器”即可!

import com.ericjin.filter.JwtVerifyFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private RsaKeyProperties prop;


    // 配置SpringSecurity相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 释放静态资源 指定资源拦截规则 指定自定义认证页面 指定退出认证配置 csrf配置
        http.csrf().disable()  // 禁用csrf
                .authorizeRequests()
                .antMatchers("/product").hasAnyRole("USER", "ADMIN")   // 表示访问所有的页面都要USER或者ADMIN角色
                .anyRequest()
                .authenticated()    // 表明其他的资源只有通过认证后才能访问
                .and()
                .addFilter(new JwtVerifyFilter(super.authenticationManager(), prop))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);   // 禁用session 不再使用
    }
}

编写产品处理器

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product")
public class ProductController {

    // @Secured("ROLE_PRODUCT")
    @RequestMapping("/findAll")
    public String findAll() {
        return "产品列表查询成功";
    }
}

启动产品服务做测试

带上token,使用9002端口查询

查询成功,成功返回信息

打开授权,需要ADMIN角色才能访问

提示错误,表示权限控制起到了作用!

posted @ 2020-05-06 16:47  Jin同学  阅读(713)  评论(0)    收藏  举报