基于SpringBoot+SpringSecurity+JWT+RSA非对称加密算法签名实现登录和权限认证

一名本科在读软件工程大三学生 —— Liujian

微笑面对生活,生活也会微笑面对你。


目录(文章过长 使用 Ctrl+F 搜索):

1、导入jar包(maven构件项目导入其坐标)
2、创建数据库、数据表及数据库连接URL(JDBC - MySQL)
3、创建实体类User、JwtUser及Dao层
4、实现UserDetailsService接口
5、编写RSA工具类
6、Token工具类
7、验证用户登录信息的拦截器
8、Token颁发服务类
9、SpringSecurity配置
10、测试认证的Controller

构建思路:

1、搭建SpringBoot工程
2、导入SpringSecurity跟Jwt的依赖
3、用户的实体类,service层,dao层
4、实现UserDetailsService接口
5、实现UserDetails接口
6、验证用户登录信息的拦截器
7、Token颁发服务类
8、SpringSecurity配置
9、用于测试认证的Controller

一、导入jar包(maven构件项目导入其坐标)

1)pom.xml

<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>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
        <version>2.3.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>1.0.10.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </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>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- jdk1.8以上没有JAXB API 需要自己引入 解决 java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter 问题 -->
    <!-- begin -->
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.0</version>
    </dependency>

    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-impl</artifactId>
        <version>2.3.0</version>
    </dependency>

    <dependency>
        <groupId>com.sun.xml.bind</groupId>
        <artifactId>jaxb-core</artifactId>
        <version>2.3.0</version>
    </dependency>

    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!-- end -->

    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>

    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>
</dependencies>

二、创建数据库、数据表及数据库连接URL(JDBC - MySQL)

1)数据库——XXX

/*
 Author: liujian
 Comment: 数据库
 Date: 19/11/2020 17:30:18
*/
CREATE DATABASE `数据库名XXX` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

2)数据表——user

/*
 Author: liujian
 Comment: 用户表
 Date: 19/11/2020 17:35:24
*/
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户密码',
  `role` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户权限',
  `status` tinyint(4) DEFAULT 0 COMMENT '是否锁定 0未锁定 1已锁定无法登陆',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

3)插入数据——密码使用Bcrypt

Bcrypt密码生成工具

INSERT INTO `user` VALUES (1, 'liujian', '$2a$10$avQ4JU0.yuTlMKOFKPr7quu.3DKXjX..T9aFR/UmTGRqFckDz6y4W', 'admin', 0);

4)数据库连接URL——MySQL

jdbc:mysql://localhost:3306/数据库名XXX?useUnicode=true&useJDBCCompliantTimezoneShift=true&serverTimezone=UTC&characterEncoding=utf8

三、创建实体类User、JwtUser及Dao层

1)实体类User

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true) // 开启链式
public class User implements Serializable {
    
    

    private Integer id;
    private String username;
    private String password;
    private String role;
    private Integer status;

    @Override
    public String toString() {
    
    
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", role='" + role + '\'' +
                ", status='" + status + '\'' +
                '}';
    }
}

2)JwtUser

该类实现了UserDetails接口,封装登录用户相关信息,例如用户ID,用户名,密码,权限集合等。

/**
 * Created by liujian on 2020/11/16 on 18:06.
 */

@Data
public class JwtUser implements UserDetails {
    
    

    private Integer id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser() {
    
    
    }

    /**
     * 直接使用User创建JwtUser的构造器
     * @param user
     */
    public JwtUser(User user) {
    
    
        this.id = user.getId();
        this.username = user.getUsername();
        this.password = user.getPassword();
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }

    @Override
    public String toString() {
    
    
        return "JwtUser{" +
                "id='" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

3)UserMapper

@Mapper
public interface UserMapper {
    
    

    User findUserByName(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liujian.security.core.dao.UserMapper">
    <resultMap id="BaseResultMap" type="com.liujian.security.core.entity.User">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="username" jdbcType="VARCHAR" property="username" />
        <result column="password" jdbcType="VARCHAR" property="password" />
        <result column="role" jdbcType="VARCHAR" property="role" />
        <result column="role" jdbcType="VARCHAR" property="role" />
        <result column="status" jdbcType="TINYINT" property="role" />
    </resultMap>
    <sql id="Base_Column_List">
        id, username, password, role, status
    </sql>
    <select id="findUserByName" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from user
        where username = #{username,jdbcType=VARCHAR}
    </select>
</mapper>

四、实现UserDetailsService接口

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    
        User user = userService.getUserByName(s);
        if (user == null) {
    
    
            throw new RuntimeException("用户" + s + "不存在");
        }
        JwtUser jwtUser = new JwtUser(user);
        // 将数据库的roles解析为UserDetails的权限集
        // AuthorityUtils.commaSeparatedStringToAuthorityList将逗号分隔的字符集转成权限对象列表
        jwtUser.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));
        return jwtUser;
    }
}

五、编写RSA工具类

1)RSA工具类

RSA工具类 密钥的创建、文件保存、读取功能(公钥和私钥)

/**
 * RSA非对称加密工具类
 * Created by liujian on 2020/11/18 21:00.
 */
public class RSAUtils {
    
    

    /**
     * 加密算法
     */
    public static final String ENCRYPT_ALGORITHM = "RSA";

    /**
     * 密钥长度
     */
    private static final int DEFAULT_KEY_SIZE = 2048;

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

    /**
     * 从文件中获取密钥
     * @param filename
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
    
    
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

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

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

    /**
     * 根据密文,生成RSA公钥和密钥,并写入文件
     * @param publicKeyFilename     公钥文件路径
     * @param privateKeyFilename    私钥文件路径
     * @param secret                生成密钥的密文
     * @param keySize               指定密钥长度,如果比默认小则选择默认长度2048
     * @throws Exception
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
    
    
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ENCRYPT_ALGORITHM);
        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 IOException {
    
    
        return Files.readAllBytes(new File(filename).toPath());
    }

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

2)RSA工具封装类RSAKeyProperties

该类用于初始化创建密钥、获取密钥(公钥和私钥)

/**
 * RSA工具封装类
 * Created by liujian on 2020/11/18 21:25.
 */
@Data
@Component
@ConfigurationProperties(prefix = "rsa.key")
public class RSAKeyProperties {
    
    

    private String publicKeyFile;
    private String privateKeyFile;

    private PublicKey publicKey;
    private PrivateKey privateKey;
    private String secret;

    @PostConstruct
    public void createRSAKey() throws Exception {
    
    
        RSAUtils.generateKey(publicKeyFile, privateKeyFile, secret, 0);
        this.publicKey = RSAUtils.getPublicKey(publicKeyFile);
        this.privateKey = RSAUtils.getPrivateKey(privateKeyFile);
    }
}

配置文件

# rsa配置
rsa.key.privateKeyFile=D:\\test\\auth\\priKey.key
rsa.key.publicKeyFile=D:\\test\\auth\\pubKey.key
rsa.key.secret=(EMOK:)_$^11244^%$_(IS:)_@@++--(COOL:)_++++_.sds_(GUY:)

六、Token工具类

1)JwtTokenUtil

该类用于生成、验证、刷新token(签名使用RSA加密技术)

  • 签名:私钥加密,公钥验证 —— 保证签名不被冒充
  • 加密:公钥加密,私钥解密 —— 保证信息不被窃取
@Data
@Component
public class JwtTokenUtil {
    
    

    @Value("${token.header}")
    private String header;

    //@Value("${token.secret}")
    //private String secret;

    @Value("${token.expiration}")
    private long expiration;

    /**
     * 生成token令牌
     * @param userDetails 用户
     * @return token令牌
     */
    public String generateToken(UserDetails userDetails, PrivateKey privateKey) {
    
    
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("sub", userDetails.getUsername());
        return generateToken(claims, privateKey);
    }

    /**
     * 从令牌中获取用户名
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token, PublicKey publicKey) {
    
    
        String username;
        try {
    
    
            Claims claims = getClaimsFromToken(token, publicKey);
            username = claims.getSubject();
        } catch (Exception e) {
    
    
            username =null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token, PublicKey publicKey) {
    
    
        try {
    
    
            Claims claims = getClaimsFromToken(token, publicKey);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
    
    
            return false;
        }
    }

    /**
     * 刷新令牌
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token, PublicKey publicKey, PrivateKey privateKey) {
    
    
        String refreshedToken;
        try {
    
    
            Claims claims = getClaimsFromToken(token, publicKey);
            refreshedToken = generateToken(claims, privateKey);
        } catch (Exception e) {
    
    
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     * @param token         令牌
     * @param userDetails   用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails, PublicKey publicKey) {
    
    
        String username = getUsernameFromToken(token, publicKey);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token, publicKey));
    }

    /**
     * 生成令牌
     * @param claims 数据声明
     * @return token令牌
     */
    private String generateToken(Map<String, Object> claims, PrivateKey privateKey) {
    
    
        Date date = new Date(System.currentTimeMillis());
        return Jwts.builder().setClaims(claims)
                .setId(createJTI())
                .setIssuedAt(date)
                .setExpiration(new Date(date.getTime() + expiration))
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 获取数据声明
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token, PublicKey publicKey) {
    
    
        Claims claims;
        try {
    
    
            claims = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).getBody();
        } catch (Exception e) {
    
    
            claims = null;
        }
        return claims;
    }

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

七、验证用户登录信息的拦截器

1)JwtAuthenticationFilter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private RSAKeyProperties rsaKeyProp;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
    
        String token = httpServletRequest.getHeader(jwtTokenUtil.getHeader());
        if(!StringUtils.isEmpty(token)) {
    
    
            String username = jwtTokenUtil.getUsernameFromToken(token, rsaKeyProp.getPublicKey());

            // 如果可以正确的从JWT中提取用户信息,并且该用户未被授权
            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    
    
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if(jwtTokenUtil.validateToken(token, userDetails, rsaKeyProp.getPublicKey())) {
    
    
                    // 给使用该JWT令牌的用户进行授权
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    // 交给spring security管理,在之后的过滤器中不会再被拦截进行二次授权了
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

八、Token颁发服务类

1)JwtAuthService

@Service
public class JwtAuthService {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private RSAKeyProperties rsaKeyProp;

    public String login(String username, String password) {
    
    
        // 用户验证
        Authentication authentication = null;
        try {
    
    
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
    
    
            throw new RuntimeException("用户验证失败");
        }
        JwtUser loginUser = (JwtUser) authentication.getPrincipal();
        // 生成Token
        return jwtTokenUtil.generateToken(loginUser, rsaKeyProp.getPrivateKey());
    }
}

九、SpringSecurity配置

1)SecurityConfig

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private JWTAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 基于token分布式认证,所以不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 配置权限
                .authorizeRequests()
                // 登录Login 验证码CaptchaImage 允许匿名访问
                .antMatchers("/login").anonymous()
                // 静态资源放行
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                // 除了上面所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                // 允许跨域访问 等同于 config类中的corsConfigurationSource
                .cors()
                .and()
                // CRSF禁用,因为不使用session,禁用跨站csrf攻击防御,否则无法登陆成功
                .csrf().disable();

        // 退出功能
        http.logout().logoutUrl("/logout");
        // 添加JWT filter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

十、测试认证的Controller

1)LoginController

@RestController
public class LoginController {
    
    

    @Autowired
    private JwtAuthService jwtAuthService;

    /**
     * 登录方法
     * 便于测试前端未使用JSON数据格式
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    @PostMapping({
    
    "/login", "/"})
    public Result login(String username, String password) {
    
    
        String token = jwtAuthService.login(username, password);
        return ResultGenerator.success("登录成功", token);
    }
}

o( ̄▽ ̄)ブ刚刚入门,欢迎批评指正 p( ^ O ^ )q,我先睡觉去了(~ o ~)~zZ==

猜你喜欢

转载自blog.csdn.net/m0_53129012/article/details/110505770