JWT+Shiro
前面我们已经了解过了一点shiro与jwt的基本知识,但是一直有个疑问,怎么让结合使用呢,
我们开始实验:由于时间有限以及授权方面有些疑问点不太懂,暂时先写出了验证部分。
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
</dependencies>
过程
首先用户要先登录,输入正确的用户名以及密码,这次只是小呆蘑,先不涉及复杂业务逻辑以及数据库操作。
编写Controller层的用户登录方法,当然这个请求应当不被shiro进行拦截:
// login登录 账号:wangyun 密码:123456
@RequestMapping("/login")
public ResponseEntity<Map<String, String>> login(String username, String password) {
log.info("username:{},password:{}",username,password);
Map<String, String> map = new HashMap<>();
// 验证用户名密码是否正确
if (!"wangyun".equals(username) || !"123456".equals(password)) {
map.put("msg", "用户名密码错误");
return ResponseEntity.ok(map);
}
JWTUtil jwtUtil = new JWTUtil();
Map<String, Object> chaim = new HashMap<>();
// 将用户信息放入payload中,注意敏感信息不要放进去。容易被非法窃取。
chaim.put("username", username);
// 这次为了验证过程,所以token设置的失效时间很长,实际开发中要根据实际业务需求进行对应修改
String jwtToken = jwtUtil.encode(username, 50 * 60 * 1000, chaim);
map.put("msg", "登录成功");
map.put("token", jwtToken);
return ResponseEntity.ok(map);
}
到这个过程我们需要一个JWTUitl工具类才行:
/**
* JWT工具类
**/
package com.siler.demo.Util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JWTUtil {
//创建默认的秘钥和算法,供无参的构造方法使用
private static final String defaultbase64EncodedSecretKey = "badbabe";
private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;
public JWTUtil() {
this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
}
private final String base64EncodedSecretKey;
private final SignatureAlgorithm signatureAlgorithm;
public JWTUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
this.signatureAlgorithm = signatureAlgorithm;
}
/*
*这里就是产生jwt字符串的地方
* jwt字符串包括三个部分
* 1. header
* -当前字符串的类型,一般都是“JWT”
* -哪种算法加密,“HS256”或者其他的加密算法
* 所以一般都是固定的,没有什么变化
* 2. payload
* 一般有四个最常见的标准字段(下面有)
* iat:签发时间,也就是这个jwt什么时候生成的
* jti:JWT的唯一标识
* iss:签发人,一般都是username或者userId
* exp:过期时间
*
* */
public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
//iss签发人,ttlMillis生存时间,claims是指还想要在jwt中存储的一些非隐私信息
if (claims == null) {
claims = new HashMap<>();
}
long nowMillis = System.currentTimeMillis();
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())//2. 这个是JWT的唯一标识,一般设置成唯一的,这个方法可以生成唯一标识
.setIssuedAt(new Date(nowMillis))//1. 这个地方就是以毫秒为单位,换算当前系统时间生成的iat
.setSubject(iss)//3. 签发人,也就是JWT是给谁的(逻辑上一般都是username或者userId)
.signWith(signatureAlgorithm, base64EncodedSecretKey);//这个地方是生成jwt使用的算法和秘钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);//4. 过期时间,这个也是使用毫秒生成的,使用当前时间+前面传入的持续时间生成
builder.setExpiration(exp);
}
return builder.compact();
}
//相当于encode的方向,传入jwtToken生成对应的username和password等字段。Claim就是一个map
//也就是拿到荷载部分所有的键值对
public Claims decode(String jwtToken) {
// 得到 DefaultJwtParser
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(base64EncodedSecretKey)
// 设置需要解析的 jwt
.parseClaimsJws(jwtToken)
.getBody();
}
//判断jwtToken是否合法
public boolean isVerify(String jwtToken) {
//这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
Algorithm algorithm = null;
switch (signatureAlgorithm) {
case HS256:
algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
break;
default:
throw new RuntimeException("不支持该算法");
}
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
//判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
return true;
}
public static void main(String[] args) {
JWTUtil util = new JWTUtil("tom", SignatureAlgorithm.HS256);
//以tom作为秘钥,以HS256加密
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
map.put("age", 20);
String jwtToken = util.encode("tom", 1000*60*60, map);
System.out.println(jwtToken);
System.out.println(util.decode(jwtToken));
util.decode(jwtToken).entrySet().forEach((entry) -> {
System.out.println(entry.getKey() + ": " + entry.getValue());
});
}
}
此时我们发现:返回信息中:
{
"msg": "登录成功",
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE2MDc2NjExMTIsImlhdCI6MTYwNzY1ODExMiwianRpIjoiODM5NzI4MjgtNDg3OC00MDJmLWI2ZmQtMzUyOWZjZGE0YWJjIiwidXNlcm5hbWUiOiJ0b20ifQ.vKsxF61Rj604xOotb02TND4XXXXXXXXXXXXXXXXXX"
}
下面我们已经登录成功,在系统中我们进行的有些操作都应该携带这个token,已验证是否是当前用户,保证合法性,当然我们要保存在请求头中(与前端需要商量一致)
首先我们需要展示shiro的配置文件信息。
@Configuration
public class ShiroConfig {
// 获取对象工厂
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultFactory();
}
@Bean
public Realm realm() {
return new JwtRealm();
}
// 配置安全管理器信息
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
/*
* b
* */
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
// 配置shiro的过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/hello");
shiroFilter.setUnauthorizedUrl("/hello");
/*
* c. 添加jwt过滤器,并在下面注册
* 也就是将jwtFilter注册到shiro的Filter中
* 指定除了login和logout之外的请求都先经过jwtFilter
* */
Map<String, Filter> filterMap = new HashMap<>();
//这个地方其实另外两个filter可以不设置,默认就是
filterMap.put("anon", new AnonymousFilter());
// 将我们自定义的JWTFilter加载进去
filterMap.put("jwt", new JWTFilter());
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>();
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
// 将过滤请求映射加入过滤器中
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
展示JWTDefaultFactory信息;
public class JwtDefaultFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
配置shiro的realm
@Slf4j
public class JwtRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
// 授权操作,暂不操作
// 大致思路是,我们可以通过JwtUtil.decode(jwt).get("username")获取到登录人信息,然后去数据库查询具有的权限信息,并根据这个权限信息赋值给该用户
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new NullPointerException("jwtToken 不允许为空");
}
//判断
JWTUtil jwtUtil = new JWTUtil();
if (!jwtUtil.isVerify(jwt)) {
throw new UnknownAccountException();
}
//下面是验证这个user是否是真实存在的
String username = (String) jwtUtil.decode(jwt).get("username");//判断数据库中username是否存在
log.info("在使用token登录"+username);
return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
//这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名
}
}
创建JWTFilter文件:
@Slf4j
public class JWTFilter extends AccessControlFilter {
// 判断是否通过
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
log.warn("isAccessAllowed 方法被调用");
return false;
}
@Override
// 判断是否拒绝通过
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
log.warn("onAccessDenied 方法被调用");
//这个地方和前端约定,要求前端将jwtToken放在请求的Header部分
//所以以后发起请求的时候就需要在Header中放一个Authorization,值就是对应的Token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
log.info("请求的 Header 中藏有 jwtToken {}", jwt);
JwtToken jwtToken = new JwtToken(jwt);
/*
* 下面就是固定写法
* */
try {
// 委托 realm 进行登录认证
//所以这个地方最终还是调用JwtRealm进行的认证
getSubject(servletRequest, servletResponse).login(jwtToken);
//也就是subject.login(token)
} catch (Exception e) {
e.printStackTrace();
onLoginFail(servletResponse);
//调用下面的方法向客户端返回错误信息
return false;
}
return true;
//执行方法中没有抛出异常就表示登录成功
}
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}
验证完毕后通过,用户可以正常进行操作。