写在前面

本文将在第三篇《整合Redis实现数据缓存》的基础上整合SpringSecurity和JWT,实现认证与授权这一功能。

使用的框架简介

SpringSecurity

SpringSecurity是一个强大的可高度定制的认证与授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。关于SpringSecurity的学习,可以参考笔者的其他文章。

JWT

JWT简介

JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

JWT组成

JWT由三部分组成:header、playload和signature,JWT token的格式为header.payload.signature

其中header中用于存放签名的生成算法:

1
{"alg": "HS512"}

payload中用于存放用户名、token的生成时间和过期时间:

1
{"sub":"admin","created":1489079981393,"exp":1489684781}

signature为以header和payload生成的签名,一旦header和payload被篡改,那么验证将失败:

1
2
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

JWT示例

如下是一个JWT的字符串:

1
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw

开发者可以点击 这里 获取到解析结果:

JWT实现认证和授权的原理

第一步,用户调用登录接口,登录成功后获取到JWT的token;

第二步,用户后续每次调用接口时,都会在http的header中添加一个名为Authorization的头,值为JWT的token;

第三步,后台程序通过对Authorization头中信息的解码及数字签名,校验来获取其中的用户信息,进而实现认证和授权。

Hutool

Hutool是一个丰富的Java开源工具包,可以帮助我们简化每一行代码,减少每一个方法。

项目使用到的表说明

由于此处采用的是RBAC(基于角色的权限控制)模型,一般会涉及到五张表,同时外加针对某个用户的特定角色,因此会涉及到六张表,分别如下所示:
(1)ums_admin:后台用户表;
(2)ums_role:后台用户角色表;
(3)ums_permission:后台用户权限表;
(4)ums_admin_role_relation:后台用户和角色关系表,用户与角色是多对多关系;
(5) ums_role_permission_relation:后台用户角色和权限关系表,角色与权限是多对多关系;
(6)ums_admin_permission_relation:后台用户和权限关系表(除角色中定义的权限以外的加减权限),加权限是指用户比角色多出的权限,减权限是指用户比角色少的权限。

整合SpringSecurity及JWT

第一步,复制一份shop-redis源码,将其名字修改为shop-springsecurity-jwt,然后对应包和文件中的信息也记得修改,本篇后续所有操作均在shop-springsecurity-jwt这一Module中进行。注意复制之后需要重新执行一下Generator类,以覆盖之前项目的自动生成文件。

第二步,在shop-springsecurity-jwt的POM文件中新增如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--SpringSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

第三步,往application.yml配置文件中在根节点下添加jwt自定义key的相关配置信息:

1
2
3
4
5
6
# 自定义jwt的key
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mySecret #JWT加解密使用的密钥
expiration: 604800 #JWT的超期限时间(60*60*24)
tokenHead: Bearer #JWT负载中拿到开头

第四步,在com.kenbings.shop.shopspringsecurityjwt.common包内新建一个名为utils的包,并在utils包内定义一个名为JwtTokenUtil的工具类,该类用于生成和解析JWT 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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"kenbings","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;

/**
* 生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}

/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}

/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}

/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}

/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}

/**
* 根据用户信息生成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 canRefresh(String token) {
return !isTokenExpired(token);
}

/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}

简单说一下其中比较重要的三个方法的作用:
(1)generateToken(UserDetails userDetails)方法,传入的是一个UserDetails对象,这是SpringSecurity提供的对象,其实就是用户登录成功后信息的一个封装对象;
(2)getUserNameFromToken(String token)方法,从token中获取用户姓名,诸如此类的方法还有很多,这里就不再说明;
(3)validateToken(String token, UserDetails userDetails)方法,用于判断token是否有效,其中token是客户端传入的,而userDetails则是从数据库中查询出来的用户信息。

第五步,在com.kenbings.shop.shopspringsecurityjwt包内新建一个名为component的包,并在component包内定义一个名为RestfulAccessDeniedHandler的类,该类定义当访问接口没有权限时,应当返回的结果,注意它需要实现AccessDeniedHandler接口,并重写其中的handle方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 当访问接口没有权限时,返回自定义的结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}

第六步,在component包内定义一个名为RestAuthenticationEntryPoint的类,该类定义当未登录或者token失效访问接口时,应当返回的结果,注意它需要实现AuthenticationEntryPoint接口,并重写其中的commence方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 当未登录或者token失效访问接口时,返回自定义的结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(e.getMessage())));
response.getWriter().flush();
}
}

第七步,修改generatorConfig.xml配置文件中的数据表信息,以生成前面所述六张表的对应信息。

第八步,在com.kenbings.shop.shopspringsecurityjwt包内新建一个名为dto的包,并在dto包内定义一个名为AdminUserDetails的类,该类定义SpringSecurity登录时需要使用到的UserDetails,注意它需要实现UserDetails接口,并重写其中对应的方法:

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
/**
* SpringSecurity登录时需要使用到的UserDetails
*/
public class AdminUserDetails implements UserDetails {
private UmsAdmin umsAdmin;
private List<UmsPermission> permissionList;

public AdminUserDetails(UmsAdmin umsAdmin,List<UmsPermission> permissionList){
this.umsAdmin = umsAdmin;
this.permissionList = permissionList;
}

/**
* 返回当前用户的权限
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissionList.stream().filter(permission -> permission.getValue()!=null)
.map(permission -> new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}

@Override
public String getPassword() {
return umsAdmin.getPassword();
}

@Override
public String getUsername() {
return umsAdmin.getUsername();
}

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

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

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

@Override
public boolean isEnabled() {
return umsAdmin.getStatus().equals(1);
}
}

请注意,正常来说这里面方法的逻辑都需要在数据库中定义对应的字段信息,然后根据业务逻辑来进行判断,而不是像这里直接固定化。这个类不需要交由Spring去管理,因此不需要添加@Component注解。

第九步,在component包内定义一个名为JwtAuthenticationTokenFilter的类,这是JWT登录授权过滤器,注意它需要继承OncePerRequestFilter类,并重写其中的doFilterInternal方法:

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
/**
* JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
//获取"Bearer "之后的部分
String authToken = authHeader.substring(this.tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("检查用户:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("当前认证用户:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}

这是在校验用户名和密码之前添加的过滤器,如果请求中包含jwt且token有效,那么会从中用户名,然后调用SpringSecurity的相关API来进行登录操作。

第十步,在service包内定义一个名为UmsAdminService的接口,用于定义后台管理员相关的接口方法:

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
/**
* 后台管理员Service
*/
public interface UmsAdminService {
/**
* 根据用户名查询后台管理员
*/
UmsAdmin getAdminByUsername(String username);

/**
* 后台管理员注册
*/
UmsAdmin register(UmsAdmin umsAdminParam);

/**
* 后台管理员登录
* @param username 用户名
* @param password 密码
* @return 生成的JWT的token
*/
String login(String username, String password);

/**
* 获取指定后台管理员的所有权限(包括角色权限和+-权限)
*/
List<UmsPermission> getPermissionList(Long adminId);
}

第十一步,在impl包内定义一个名为UmsAdminServiceImpl的类,这个类需要实现UmsAdminService接口,并重写其中的方法:

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
/**
* 后台管理员Service实现类
*/
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class);

@Value("${jwt.tokenHead}")
private String tokenHead;

@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UmsAdminMapper adminMapper;
@Autowired
private UmsAdminRoleRelationDao adminRoleRelationDao;


@Override
public UmsAdmin getAdminByUsername(String username) {
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(username);
List<UmsAdmin> adminLists = adminMapper.selectByExample(example);
if(adminLists !=null && !adminLists.isEmpty()){
return adminLists.get(0);
}
return null;
}

@Override
public UmsAdmin register(UmsAdmin umsAdminParam) {
//查询是否有相同用户名的用户
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(umsAdminParam.getUsername());
List<UmsAdmin> umsAdminList = adminMapper.selectByExample(example);
if (!umsAdminList.isEmpty()) {
return null;
}
UmsAdmin umsAdmin = new UmsAdmin();
BeanUtils.copyProperties(umsAdminParam, umsAdmin);
umsAdmin.setCreateTime(new Date());
umsAdmin.setStatus(1);
//将密码进行加密操作
String encodePassword = passwordEncoder.encode(umsAdmin.getPassword());
umsAdmin.setPassword(encodePassword);
adminMapper.insert(umsAdmin);
return umsAdmin;
}

@Override
public String login(String username, String password) {
String token = null;
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails == null){
throw new BadCredentialsException("不存在对应的用户信息");
}
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}

@Override
public List<UmsPermission> getPermissionList(Long adminId) {
return adminRoleRelationDao.getPermissionList(adminId);
}
}

可以看到在后面我们定义了getPermissionList(Long adminId)方法,该方法用于根据adminId从数据库中查询用户所有的权限,包括角色权限以及个人独有的权限。

第十二步,在com.kenbings.shop.shopspringsecurityjwt包内新建一个名为dao的包,并在dao包内定义一个名为UmsAdminRoleRelationDao的接口,这是一个自定义的dao,用于查询后台用户与角色:

1
2
3
4
5
6
7
8
9
/**
* 用于查询后台用户与角色的自定义DAO
*/
public interface UmsAdminRoleRelationDao {
/**
* 获取用户所有权限(包括+-权限)
*/
List<UmsPermission> getPermissionList(@Param("adminId") Long adminId);
}

第十三步,在resources目录下新建一个名为mapper的目录,并在mapper目录下定义一个名为UmsAdminRoleRelationDao.xml的XML文件,这个其实就是之前UmsAdminRoleRelationDao接口对应的XML文件:

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
<?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.kenbings.shop.shopspringsecurityjwt.dao.UmsAdminRoleRelationDao">
<select id="getPermissionList" resultMap="com.kenbings.shop.shopspringsecurityjwt.mbg.mapper.UmsPermissionMapper.BaseResultMap">
SELECT
p.*
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role r ON ar.role_id = r.id
LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
LEFT JOIN ums_permission p ON rp.permission_id = p.id
WHERE
ar.admin_id = #{adminId}
AND p.id IS NOT NULL
AND p.id NOT IN (
SELECT
p.id
FROM
ums_admin_permission_relation pr
LEFT JOIN ums_permission p ON pr.permission_id = p.id
WHERE
pr.type = - 1
AND pr.admin_id = #{adminId}
)
UNION
SELECT
p.*
FROM
ums_admin_permission_relation pr
LEFT JOIN ums_permission p ON pr.permission_id = p.id
WHERE
pr.type = 1
AND pr.admin_id = #{adminId}
</select>
</mapper>

第十四步,在config包内定义一个名为SecurityConfig的类,这是SpringSecurity的配置类,它需要继承WebSecurityConfigurerAdapter类,并实现其中的一些方法:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* SpringSecurity配置类
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UmsAdminService adminService;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

/**
* 用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器
* @param httpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()// 基于token,所以不需要session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs/**"
)
.permitAll()
.antMatchers("/admin/login", "/admin/register")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
.permitAll()
// .antMatchers("/**")//测试时全部运行访问
// .permitAll()
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}

/**
* 用于配置UserDetailsService及PasswordEncoder
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}

/**
* SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
UmsAdmin admin = adminService.getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}

/**
* 在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录
* @return
*/
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}

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

简单解释一下上述一些方法:
(1)configure(HttpSecurity httpSecurity)方法,用于配置需要拦截的url路径、jwt过滤器及出现异常后的处理器;
(2)configure(AuthenticationManagerBuilder auth)方法,用于配置UserDetailsService及PasswordEncoder;
(3)passwordEncoder()方法,这是SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
(4)userDetailsService()方法,这是SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要开发者自行实现;
(5)jwtAuthenticationTokenFilter()方法,这是在校验用户名和密码前添加的过滤器,如果有jwt且token有效,那么会自行根据token信息进行登录;
(6)RestfulAccessDeniedHandler,这是当访问接口没有权限时,返回自定义的JSON格式结果;
(7)RestAuthenticationEntryPoint,这是当未登录或者token失效访问接口时,返回自定义的JSON格式结果。

第十五步,在dto包内定义一个名为UmsAdminLoginParam的类,这是后台管理员登录参数:

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
/**
* 后台管理员登录参数
*/
public class UmsAdminLoginParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true)
@NotEmpty(message = "密码不能为空")
private String password;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

第十六步,在controller包内定义一个名为UmsAdminController的类,这是后台管理员登录注册功能的Controller:

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
/**
* 后台管理员登录注册功能Controller
*/
@RestController
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;

@Autowired
private UmsAdminService adminService;

@ApiOperation(value = "用户注册")
@PostMapping("/register")
public CommonResult<UmsAdmin> register(@RequestBody UmsAdmin umsAdminParam, BindingResult result) {
if (result.hasErrors()) {
return CommonResult.failed(result.getFieldError().getDefaultMessage());
}
UmsAdmin umsAdmin = adminService.register(umsAdminParam);
if (umsAdmin == null) {
return CommonResult.failed();
}
return CommonResult.success(umsAdmin);
}

@ApiOperation(value = "登录后返回token")
@PostMapping("/login")
public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam, BindingResult result) {
if (result.hasErrors()) {
return CommonResult.failed(result.getFieldError().getDefaultMessage());
}
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}

@ApiOperation("获取用户所有权限(包括+-权限)")
@GetMapping("/permission/{adminId}")
@ResponseBody
public CommonResult<List<UmsPermission>> getPermissionList(@PathVariable @ApiParam("后台用户id")Long adminId) {
List<UmsPermission> permissionList = adminService.getPermissionList(adminId);
return CommonResult.success(permissionList);
}
}

可以看到这里我们只提供了三个接口,分别是后台用户登录、注册以及获取对应权限的接口。

第十七步,修改config包中Swagger2Config类的代码为如下所示,这里其实就是实现接口调用时自带Authorization头,后续就可以访问需要登录的接口:

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
/**
* Swagger2 API文档相关配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.kenbings.shop.shopspringsecurityjwt.controller"))
.paths(PathSelectors.any())
.build()
//添加登录认证
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("整合SwaggerUI")
.description("myshop-all")
.contact("kenbings")
.version("1.0")
.build();
}

private List<ApiKey> securitySchemes() {
//设置请求头信息
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
result.add(apiKey);
return result;
}

private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/brand/.*"));
return result;
}

private SecurityContext getContextByPath(String pathRegex){
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}

private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}

可以看到我们在securityContexts()方法中设置了需要登录认证的路径,即品牌相关的接口访问都需要登录之后才能访问。

第十八步,修改之前PmsBrandController类中的方法,我们给它们添加如下访问权限:
(1)给添加接口添加pms:brand:create权限:

1
2
@PreAuthorize("hasAuthority('pms:brand:create')")
public CommonResult createBrand(@RequestBody PmsBrand pmsBrand)

(2)给删除接口添加pms:brand:delete权限:

1
2
@PreAuthorize("hasAuthority('pms:brand:delete')")
public CommonResult deleteBrand(@PathVariable("id") Long id)

(3)给修改接口添加pms:brand:update权限:

1
2
3
4
5
6
@PreAuthorize("hasAuthority('pms:brand:update')")
public CommonResult updateBrand(
@PathVariable("id") Long id,
@RequestBody PmsBrand pmsBrandDTO,
BindingResult result
)

(4)给查询接口添加pms:brand:read权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
@PreAuthorize("hasAuthority('pms:brand:read')")
public CommonResult<List<PmsBrand>> getBrandList()

@PreAuthorize("hasAuthority('pms:brand:read')")
public CommonResult<CommonPage<PmsBrand>> listBrand(
@RequestParam(value = "pageNum",defaultValue = "1")
@ApiParam("页码")Integer pageNum,
@RequestParam(value = "pageSize",defaultValue = "5")
@ApiParam("每页数量") Integer pageSize
)

@PreAuthorize("hasAuthority('pms:brand:read')")
public CommonResult<PmsBrand> getBrand(@PathVariable("id")Long id)

注意这里面的权限就是ums_permission数据表中value字段的值,因此值必须按照要求进行书写:

第十九步,修改项目启动类ShopSpringSecurityJwtApplication,在其上面使用@MapperScan注解来指定扫描的mapper接口路径,注意我们的mapper接口路径有两个,因此需要使用字符串数组来进行设置:

1
2
3
4
5
6
7
@SpringBootApplication
@MapperScan({"com.kenbings.shop.shopspringsecurityjwt.mbg.mapper","com.kenbings.shop.shopspringsecurityjwt.dao"})
public class ShopSpringSecurityJwtApplication {
public static void main(String[] args) {
SpringApplication.run(ShopSpringSecurityJwtApplication.class, args);
}
}

第二十步,启动项目,访问Swagger-UI接口文档地址,即浏览器访问http://localhost:8080/swagger-ui.html链接,可以看到新的接口已经出现了:

首先是未登录,访问/admin/permission/{adminId}接口,可以看到返回之前定义的JSON信息:

然后我们尝试进行登录,登录账号为kenbings,密码为kenbings,访问/admin/login接口:

可以看到我们已经登录成功了,接下来点击右侧的Authorize按钮,在弹框中输入登录接口中获取到的token信息:

之后我们再来访问之前的/admin/permission/{adminId}接口,可以看到该接口已经可以正常访问了:

然后我们再来访问一些需要权限的接口,如获取商品品牌管理中获取所有品牌信息的接口/brand/listAll

这是正常结果,因为当前用户没有任何权限,我们给这个用户可以访问该接口的权限,接着再去访问一下该接口,可以看到数据能正常返回了:

这样本篇关于整合SpringSecurity和JWT实现认证与授权的学习就完成了,后续介绍如何整合ElasticSearch实现商品搜索这一功能。本篇笔记源码,可以点击 这里 进行阅读。