Spring Security 分布式认证

相关介绍

1)JWT
JWT:全称 JSON Web Token,是一个分布式身份校验方案,可生产 token,也可解析 token
JWT生成的 token 由三部分组成:

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

2)RSA 非对称加密
基本原理:同时生成两把密钥(公钥和私钥),私钥隐秘保存,公钥可发放给信任的客户端

  • 私钥加密:持有私钥或公钥可解密
  • 公钥加密:持有私钥才可解密

优点:安全,难以破解
缺点:算法耗时,但可以接受

3)项目说明
在这里插入图片描述

以下只贴关键代码,需要完整代码见本文末尾 github 链接

公共模块

1)首先创建一个父工程,把其中 src 目录删除,添加 pom 依赖,主要添加 springboot 依赖


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

2)在该工程下创建子模块----公共模块
该模块放了一些 JWT 和 RSA 的工具类,因此需要加入 jwt 和 json 相关的依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springsecurity_jwt_parent</artifactId>
        <groupId>com.xiao</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common_module</artifactId>


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

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

具体工具类见源码>>源码

认证模块

1)认证模块,主要对用户信息进行校验并生成 token。也算是一个完整的项目,需要实体类(用户信息、角色信息)、service 、mapper等。
这些类的代码可以参考《Spring Security 简单认证与授权

扫描二维码关注公众号,回复: 13298490 查看本文章

2)配置文件

rsa:
  key:
    private-key-path: G:\temp\authRsa\rsa_id_key
    public-key-path: G:\temp\authRsa\rsa_id_key.pub

这个密钥文件可以通过工具类的 密钥生成方法生成,代码在公共模块的测试类中

3)JWT 的认证和验证 过滤器

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    
	//过滤器不放到容器中,无法直接注入,需通过构造器传入
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties rsaKeyProperties;

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

	//认证逻辑
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
    
    

        try {
    
    
        	//从请求中获取用户信息
            SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            UsernamePasswordAuthenticationToken authRequest =
                    new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
            return authenticationManager.authenticate(authRequest);
        } catch (IOException e) {
    
    
        	//认证失败,返回失败信息
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            try {
    
    
                PrintWriter writer = response.getWriter();
                Map resultMap = new HashMap<>();
                resultMap.put("code",HttpServletResponse.SC_UNAUTHORIZED);
                resultMap.put("msg", "用户名或密码错误!");
                writer.write(new ObjectMapper().writeValueAsString(resultMap));
                writer.flush();
                writer.close();
            }catch(Exception ex) {
    
    
                ex.printStackTrace();
            }
            throw new RuntimeException(e);
        }

    }

	// 认证成功后,生成 token,返回 token
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    
    
        SysUser sysUser = new SysUser();
        sysUser.setUsername(authResult.getName());  // 密码是敏感信息,不放到头部
        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());
        String token = JwtUtils.generateTokenExpireInMinutes(sysUser, rsaKeyProperties.getPrivateKey(), 24 * 60);
        response.addHeader("Authorization", "Bearer " + token);
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        try {
    
    
            PrintWriter writer = response.getWriter();
            Map resultMap = new HashMap<>();
            resultMap.put("code",HttpServletResponse.SC_OK);
            resultMap.put("msg", "认证通过!");
            writer.write(new ObjectMapper().writeValueAsString(resultMap));
            writer.flush();
            writer.close();
        }catch(Exception ex) {
    
    
            ex.printStackTrace();
        }
    }


}
//验证token是否正确
public class JwtVerifyFilter extends BasicAuthenticationFilter {
    
    
    private RsaKeyProperties rsaKeyProperties;

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

    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    
        String header = request.getHeader("Authorization");
        if(header == null || !header.startsWith("Bearer")){
    
    
            chain.doFilter(request, response);
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter writer = response.getWriter();
            Map 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是否正确
            Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
            //获取到当前登录用户的信息
            SysUser userInfo = payload.getUserInfo();
            if(userInfo != null) {
    
    
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), "", userInfo.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            }
        }

    }

}

4)配置类主要需要配置 RSA 和 springsecurity
从配置文件绑定属性到实体类中

@ConfigurationProperties(prefix = "rsa.key")
public class RsaPath {
    
    
    private String publicKeyPath;
    private String privateKeyPath;

    public String getPublicKeyPath() {
    
    
        return publicKeyPath;
    }

    public void setPublicKeyPath(String publicKeyPath) {
    
    
        this.publicKeyPath = publicKeyPath;
    }

    public String getPrivateKeyPath() {
    
    
        return privateKeyPath;
    }

    public void setPrivateKeyPath(String privateKeyPath) {
    
    
        this.privateKeyPath = privateKeyPath;
    }

}
@Component
public class RsaKeyProperties {
    
    

    @Autowired
    RsaPath rsaPath;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct     //该注释确保publicKeyPath和privateKeyPath都有值后才执行该方法
    public void getRsaKey() throws Exception {
    
    
        System.out.println(rsaPath.getPublicKeyPath());
        publicKey = RsaUtils.getPublicKey(rsaPath.getPublicKeyPath());
        privateKey = RsaUtils.getPrivateKey(rsaPath.getPrivateKeyPath());
    }

    public PublicKey getPublicKey() {
    
    
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
    
    
        this.publicKey = publicKey;
    }

    public PrivateKey getPrivateKey() {
    
    
        return privateKey;
    }

    public void setPrivateKey(PrivateKey privateKey) {
    
    
        this.privateKey = privateKey;
    }
}
@Configuration
@EnableWebSecurity
public class SpringsecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private SysUserService userService;
    @Autowired
    private RsaKeyProperties rsaKeyProperties;

    //配置加密
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder(); //spring security 内置加密算法
    }

    //认证用户的来源
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    //Spring Security配置
    public void configure(HttpSecurity hs) throws Exception {
    
    
        hs.csrf()
            .disable()
            .authorizeRequests()
            .antMatchers("/**").hasAnyRole("NORMAL")
            .anyRequest().authenticated()

            .and()  //绑定过滤器
            .addFilter(new JwtLoginFilter(super.authenticationManager(), rsaKeyProperties))
            .addFilter(new JwtVerifyFilter(super.authenticationManager(), rsaKeyProperties))
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //分布式不需要session
    }
}

5)测试
使用 postman 进行测试(用到的数据库数据可参考上一篇《Spring Security 简单认证与授权》)
登录认证 > 携带返回的 token 访问资源
在这里插入图片描述

在这里插入图片描述

资源模块

该模块大部分内容与认证模块相同。该模块不能放置私钥,所以需要把跟私钥相关的信息删除即可。跟认证的逻辑有关的内容也要删除,因为该模块不需要用户登录,只校验其携带的 token。

该模块主要是需要校验请求携带的 token 是否有效,有效则允许访问资源,否则拒绝访问。

具体代码可参考源码

测试
无需进行登录,把刚刚在认证模块返回的、还未失效的 token 拿过来去请求访问资源即可。

本项目 github 地址:https://github.com/godXiaogf/springsecurity_jwt_parent_idea

猜你喜欢

转载自blog.csdn.net/Alias_fa/article/details/106967472