一名本科在读软件工程大三学生 —— 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
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==