Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

SpringSecurity+JWT实现的登录认证

1、简介

  1. Spring Security
    在 Web 编程开发中,登录安全往往也是很重要的一个部分,而 Spring Security 所做得就是这个工作。在 java 领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security 等两种技术选型。Apache Shiro 简单易用也算是一大优势,但其功能还是远不如 Spring Security 强大。后者可以为应用提供声明式的安全访问限制,他提供了一系列的可以由开发者主动配置的 bean ,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。

  2. JWT
    JWT 的全称是:Json Web Token 。是在网路应用中传递信息的一种基于 json 的开发标准,可用于作为 json 对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在身份提供者和服务提供者间传递被认证的用户身份信息。

2、设计登录认证所用的表

本次登录认证需要用到三个表,分别是用户表,身份表以及用户和身份绑定的表。``

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for hibernate_sequence
-- ----------------------------
DROP TABLE IF EXISTS `hibernate_sequence`;
CREATE TABLE `hibernate_sequence` (
`next_val` bigint(0) NULL DEFAULT NULL
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed;

-- ----------------------------
-- Records of hibernate_sequence
-- ----------------------------
INSERT INTO `hibernate_sequence` VALUES (1);
INSERT INTO `hibernate_sequence` VALUES (1);

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(0) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_NORMAL');
INSERT INTO `role` VALUES (2, 'ROLE_ADMIN');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(0) NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------

-- ----------------------------
-- Table structure for user_roles
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
`user_id` bigint(0) NULL DEFAULT NULL,
`roles_id` bigint(0) NULL DEFAULT NULL,
INDEX `FKj9553ass9uctjrmh0gkqsmv0d`(`roles_id`) USING BTREE,
INDEX `FK55itppkw3i07do3h7qoclqd4k`(`user_id`) USING BTREE,
CONSTRAINT `FK55itppkw3i07do3h7qoclqd4k` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FKj9553ass9uctjrmh0gkqsmv0d` FOREIGN KEY (`roles_id`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_roles
-- ----------------------------
INSERT INTO `user_roles` VALUES (1, 2);
INSERT INTO `user_roles` VALUES (1, 1);

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint(0) NOT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'chenyicai', '$2a$10$hZG8XjmuAcuY.izAj0D7wuQIvPDdwdz.y4KcbGPI18Mri1hx1FWA6');

SET FOREIGN_KEY_CHECKS = 1;

This is a picture without description

This is a picture without description

This is a picture without description

3、创建新工程并导入依赖及实体类

  1. 在创建一个SpringBoot工程之后我们需要引入Security和JWT必须的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
    </dependency>
  2. 在配置文件中配置数据库信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jwtdemo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: chenyicai
    password: cyc1234
    ackson:
    serialization:
    indent_output: true
    server:
    port: 8086
    #日志级别(一般设置为INFO)
    logging:
    level:
    cn:
    edu:
    guet:
    mapper: debug
    org:
    springframework:
    security: info
    mybatis:
    type-aliases-package: cn.edu.guet.entity
    mapper-locations: classpath:mapper/*.xml
  3. 创建用户以及身份实体类

Users类:(省略 getset 方法)

此处所创建的 User 类继承了 Spring Security 的 UserDetails 接口,从而成为了一个符合 Security 安全的用户,即通过继承 UserDetails,即可实现 Security 中相关的安全功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class User implements UserDetails {

private Long id;

private String username;

private String password;

private List<Role> roles;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add( new SimpleGrantedAuthority( role.getName() ) );
}
return authorities;
}
@Override
public String getUsername() {
return username;
}

@Override
public String getPassword() {
return password;
}

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

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

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

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

Role类:(省略 getset 方法)

1
2
3
4
5
6
7
public class Role {

private Long id;

private String name;

}

4、JWT工具类

该工具类主要用于对 JWT Token 进行各项操作,比如生成Token、验证Token、刷新Token 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Component
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;

private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_AUTHORITIES="authorities";

// 生成token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put( CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}

// 验证token
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return !isTokenExpired(token);
}
// 刷新token
public String refreshToken(String token){
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
// 验证token是否失效
public boolean isTokenExpired(String token){
Date expireDate = getExpiredDateFromToken(token);
return expireDate.before(new Date());
}
// 从token中获取过期时间
public Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
// 从token中获取用户名
public String getUserNameFromToken(String token){
String username;
try{
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
// 从token中获取荷载
private Claims getClaimsFromToken(String token){
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(Const.SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e){
e.printStackTrace();
}
return claims;
}
// 生成过期时间
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis()+Const.EXPIRATION_TIME*1000);
}
// 根据荷载生成token
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, Const.SECRET )
.compact();
}
}

这里还有一个常量类,存储了JWT的一些常量信息

1
2
3
4
5
6
7
8
9
10
11
12
public class Const {

// 5天(以毫秒ms计)
public static final long EXPIRATION_TIME = 432_000_000;
// JWT密码
public static final String SECRET = "CodeSheepSecret";
// Token前缀
public static final String TOKEN_PREFIX = "Bearer";
// 存放Token的Header Key
public static final String HEADER_STRING = "Authorization";
}

5、Token过滤器

用于每次外部对接口请求时的Token处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class JwtFilter extends OncePerRequestFilter {

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private JwtTokenUtils jwtTokenUtil;

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 确认是否能根据key拿到value
String authHeader = httpServletRequest.getHeader( Const.HEADER_STRING );
if (authHeader == null) {
authHeader =Const.TOKEN_PREFIX+" "+httpServletRequest.getHeader("token");
}
// 判断登录用户的token不为空和是Bearer开头的
if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
// 取到token
final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
// 从用户请求携带的token获取用户名,能取到证明token除了时间以外都合法了
String username = jwtTokenUtil.getUserNameFromToken(authToken);
System.out.println(username);
// token 存在用户名但没有认证的
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 根据userDetails验证了token是否有效(验证时间是否过期和当前用户名是否匹配)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 我们的token,框架是不认识的,token有效就转化构建 UsernamePasswordAuthenticationToken表示认证通过和进行相关授权
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
httpServletRequest));
// 设置了认证主体,到UsernamePasswordAuthenticationFilter就不会拦截,因为你应该带有了它的token
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
// 继续执行其他过滤器
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}

6、Service层业务操作

主要是登录以及注册的业务

首先是AuthService接口:

1
2
3
4
public interface AuthService {
User register( User userToAdd );
String login( String username, String password );
}

AuthServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class AuthServiceImpl implements AuthService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private UserRepository userRepository;

// 登录
@Override
public String login( String username, String password ) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}

// 注册
@Override
public User register( User userToAdd ) {
final String username = userToAdd.getUsername();
if( userRepository.findByUsername(username)!=null ) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword( encoder.encode(rawPassword) );
return userRepository.save(userToAdd);
}
}

然后是 UserService 实现类,它实现了 UserDetailsService,可用于在登录认证时检验用户的身份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.findByUsername(s);
List<Role> roleList =userMapper.findRoleByUsername(s);
user.setRoles(roleList);
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getAuthorities());
System.out.println(user.getRoles());
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}

7、Spring Security配置类编写

这是比较重要的一点,这里决定了拦截的页面以及其他的一些操作。这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure 配置,来实现用户自定义的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;

@Bean
public JwtFilter authenticationTokenFilterBean() throws Exception {
return new JwtFilter();
}

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

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

@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS请求全部放行
.antMatchers(HttpMethod.POST, "/authentication/**").permitAll() //登录和注册的接口放行,其他接口全部接受验证
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();

// 使用前文自定义的 Token过滤器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

httpSecurity.headers().cacheControl();
}
}

8、接下来编写Controller类进行测试

首先是登陆注册的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class JwtAuthController {

@Autowired
private AuthService authService;

// 登录
@RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
public String createToken( String username,String password ) throws AuthenticationException {
return authService.login( username, password );
}

// 注册
@RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
public User register( @RequestBody User addedUser ) throws AuthenticationException {
return authService.register(addedUser);
}

}

然后是测试权限的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class TestController {

// 测试普通权限
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
@RequestMapping( value="/normal/test", method = RequestMethod.GET )
public String test1() {
return "ROLE_NORMAL /normal/test接口调用成功!";
}

// 测试管理员权限
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping( value = "/admin/test", method = RequestMethod.GET )
public String test2() {
return "ROLE_ADMIN /admin/test接口调用成功!";
}
}

9、现在我们进行测试

  1. 首先测试能否正常登录并获取到TokenThis is a picture without description
    可以看到在账号密码正确的情况下,能返回一个Token,接下来只要访问网页时带上这个Token,就可以顺利进行访问

  2. 接下来测试能否正常访问网页
    首先是不带Token的情况:
    This is a picture without description
    很明显,被拒绝访问了。
    接下来我们测试一下携带Token的情况:
    This is a picture without description
    可以成功访问。