JWT实战

一、在pom文件中引入jwt的依赖包

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>

二、写一个工具类用于生成签名和验证签名

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
public class JwtUtil {

//JWT-account
public static final String ACCOUNT = "username";
//JWT-currentTimeMillis
public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
//有效期时间2小时
public static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
//秘钥
public static final String SECRET_KEY = "shirokey";


/**
* 生成签名返回token
*
* @param account
* @param currentTimeMillis
* @return
*/
public static String sign(String account, String currentTimeMillis) {
// 帐号加JWT私钥加密
String secret = account + SECRET_KEY;
// 此处过期时间,单位:毫秒,在当前时间到后边的20分钟内都是有效的
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//采用HMAC256加密
Algorithm algorithm = Algorithm.HMAC256(secret);

return JWT.create()
.withClaim(ACCOUNT, account)
.withClaim(CURRENT_TIME_MILLIS, currentTimeMillis)
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
}

/**
* 校验token是否正确
*
* @param token
* @return
*/
public static boolean verify(String token) {
String secret = getClaim(token, ACCOUNT) + SECRET_KEY;
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
verifier.verify(token);
return true;
}

/**
* 获得Token中的信息无需secret解密也能获得
*
* @param token
* @param claim
* @return
*/
public static String getClaim(String token, String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}

}

三、封装自己的token,用于后边校验token类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JwtToken implements AuthenticationToken {

private final String token;

public JwtToken(String token){
this.token = token;
}

@Override
public Object getPrincipal() {
return token;
}

@Override
public Object getCredentials() {
return token;
}

四、我们需要在登陆时创建token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//service中的登录处理
@Override
public UserTokenDTO login(UserTokenDTO userInfo) {
// 从数据库获取对应用户名密码的用户
SysUserInfo uInfo = userInfoMapper.getUserByLogin(userInfo.getName());
if (null == uInfo) {
//用户信息不存在
throw new BusinessException(CommonResultStatus.USERNAME_ERROR);
} else if (!userInfo.getPassword().equals(uInfo.getPassword())) {
//密码错误
throw new BusinessException(CommonResultStatus.PASSWORD_ERROR);
}
//生成jwtToken
userInfo.setToken(JwtUtil.sign(userInfo.getName(),String.valueOf(System.currentTimeMillis())));
return userInfo;
}

五、在其他需要登录后才能访问的请求中解析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
public class JwtFilter extends AccessControlFilter {

//设置请求头中需要传递的字段名
protected static final String AUTHORIZATION_HEADER = "Access-Token";

/**
* 表示是否允许访问,mappedValue就是[urls]配置中拦截器参数部分,
* 如果允许访问返回true,否则false
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @param mappedValue:
* @return: boolean
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}

/**
* 表示当访问拒绝时是否已经处理了,
* 如果返回true表示需要继续处理,
* 如果返回false表示该拦截器实例已经处理了,将直接返回即可
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @return: boolean
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
// 解决跨域问题
if(HttpMethod.OPTIONS.toString().matches(req.getMethod())) {
return true;
}
if (isLoginAttempt(request, response)) {
//生成jwt token
JwtToken token = new JwtToken(req.getHeader(AUTHORIZATION_HEADER));
//委托给Realm进行验证
try {
//调用登陆会走Realm中的身份验证方法
getSubject(request, response).login(token);
return true;
} catch (Exception e) {
}
}else{
throw new BusinessException(CommonResultStatus.LOGIN_ERROR);
}

return false;
}



/**
* 判断是否有头部参数
* @author cheetah
* @date 2020/11/24
* @param request:
* @param response:
* @return: boolean
*/
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(AUTHORIZATION_HEADER);
return authorization != null;
}

}

六、当滤器中调用subject.login(token)方法时,会走自定义Realm中的doGetAuthenticationInfo(AuthenticationToken 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
@Slf4j
public class JwtRealm extends AuthorizingRealm {

@Autowired
private UserInfoService userService;

//验证是不是自己的token类型
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}

/**
* 身份验证
* @author cheetah
* @date 2020/11/25
* @param token:
* @return: org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String credentials = (String) token.getCredentials();
String username = null;
try {
//jwt验证token
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
username = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
throw new BusinessException(CommonResultStatus.TOKEN_CHECK_ERROR,e.getMessage());
}

//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, //用户名
credentials, //凭证
getName() //realm name
);
return authenticationInfo;
}

/**
* 权限校验(次数不做过多讲解)
* @author cheetah
* @date 2020/11/25
* @param principals:
* @return: org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// String username = principals.toString();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//角色权限暂时不加
// authorizationInfo.setRoles(userService.getRoles(username));
// authorizationInfo.setStringPermissions(userService.queryPermissions(username));
return authorizationInfo;
}

}

七、接下来我们需要修改ShiroConfig文件,将自定义的Filter和Realm交由SecurityManager进行管理

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

/**
此类较长,只展示部分重要代码,其余代码可在公众号“阿Q说”中回复"jwt"获取源码
**/
@Configuration
@Slf4j
public class ShiroConfig {

/**
* 创建ShiroFilterFactoryBean
* @author cheetah
* @date 2020/11/21
* @return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置shiro内置过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
//添加自定义过滤器:只对需要登陆的接口进行过滤
filters.put("authc", new JwtFilter());
//添加自定义过滤器:对权限进行验证
// filters.put("roles", new CustomRolesOrAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);

// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/adminLogin/login");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notAuth");

// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//游客,开发权限
filterChainDefinitionMap.put("/guest/**", "anon");
//用户,需要角色权限 “user”
filterChainDefinitionMap.put("/user/**", "roles[user]");
// filterChainDefinitionMap.put("/productInfo/**", "roles[user]");
//管理员,需要角色权限 “admin”
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//开放登陆接口
filterChainDefinitionMap.put("/adminLogin/login", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("-------Shiro拦截器工厂类注入成功-----------");
return shiroFilterFactoryBean;
}

/**
* 注入安全管理器
* @author cheetah
* @date 2020/11/21
* @return: java.lang.SecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm, SubjectFactory subjectFactory,
SessionManager sessionManager,
CacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jwtRealm);

//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setSubjectFactory(subjectFactory);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(cacheManager);
return securityManager;
}

/**
* jwt身份认证和权限校验
* @author cheetah
* @date 2020/11/24
* @return: com.cheetah.shiroandjwt.jwt.JwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
jwtRealm.setAuthenticationCachingEnabled(true);
jwtRealm.setAuthorizationCachingEnabled(true);
return jwtRealm;
}
}