JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
Header: 由典型的两部分组成->token类型(JWT)和算法名称(HMAC/SHA256/RSA/等等),一般情况下Header是不变的
Payload: 声明部分一般包含过期时间,用户名密码等等信息具体可以根据Jwt生成Utils类自定义,如下
public String generateJwt(String username, String password){
if (StringUtils.isBlank(username)||StringUtils.isBlank(password)){
return null;
}
String token=Jwts.builder().setSubject(subject)
.claim("un",username)
.claim("pwd",password)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+expire))
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
return token;
}
Signature: 签名部分的生成依赖于前面两部分和秘钥,
1、分布式场景下的鉴权或者Oauth
2、用作信息交换
自定义token实在AuthenticationToken基础上写的一个shiro令牌
public class JwtToken implements AuthenticationToken, Serializable {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public String toString() {
return "JwtToken{" +
"token='" + token + '\'' +
'}';
}
最常见不过的配置类,其中自定义了两个filters,在进行Jwt进行过滤的时候
难免会出现路径已经过滤的情况,这时候我这边出现了个问题是只有第一个Filter生效,后面的不起作用,将在文章末为大家提供详细解决方案
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager manager/*, RolesFilter rolesFilter*/){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
// 必须设置SecurityManager
bean.setSecurityManager(manager);
// 未登录,是否执行是根据subject.login判断的
bean.setLoginUrl("/api/v1/gate/user/pub/needLogin");
bean.setSuccessUrl("/");
// 无权限
bean.setUnauthorizedUrl("/api/v1/gate/user/pub/notPermit");
// 设置自定义filter
Map filters=new TreeMap<>();
filters.put("jwt",new JwtFilter());
filters.put("roles",new RolesFilter());
bean.setFilters(filters);
// 路径拦截顺序,一定要使用LinkedHashMap,否则时而拦截,时而不拦截
Map map=new LinkedHashMap<>();
// 登出
map.put("/api/v1/gate/user/logout","logout");
// 游客模式
map.put("/api/v1/gate/user/pub/**","anon");
map.put("/a/b","jwt");
// 登录才可以
map.put("/api/v1/gate/user/authc/**","authc");
// 需要admin用户权限才可以访问
map.put("/api/v1/gate/user/admin/**","roles[root]");
map.put("/api/v1/gate/user/goods/video/update","perms[video_update]");
bean.setFilterChainDefinitionMap(map);
return bean;
}
/**
* 认证管理
* @return
*/
@Bean
public SecurityManager getSecurityManager(){
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
manager.setRealm(getJwtRealm());
return manager;
}
/**
* 自定义Realm
* @return
*/
@Bean
public JwtRealm getJwtRealm(){
JwtRealm jwtRealm=new JwtRealm();
return jwtRealm;
}
自定义的Jwt拦截器,目的在于拦截请求,看请求头中是否包含“token”
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getHeader(DEFAULT_JWT_HEADER) != null) {
log.info("接口请求头携带token");
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
e.printStackTrace();
}
}else{
log.info("接口请求头未携带token");
sendJsonMessage((HttpServletResponse) response,JsonData.buildError(-1,"未发现token,请登录"));
}
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(DEFAULT_JWT_HEADER);
JwtToken jwtToken = new JwtToken(token);
getSubject(request, response).login(jwtToken);
return true;
}
为登录认证于鉴权提供保证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) {
log.info("执行认证...");
// 强转成自定义的JwtToken
JwtToken jwt=(JwtToken)authToken;
// 获取token
String token=(String)jwt.getCredentials();
// 先检查能否取出用户
String username=jwtConfig.getUsername(token);
if(username==null){
throw new AuthenticationException("token无效");
}
// 查找数据库,看是否存有当前用户
User user=userService.selectUserByUsername(username);
if(user==null){
throw new AuthenticationException("用户不存在") ;
}
if(!jwtConfig.verify(token,user.getPassword())){
throw new AuthenticationException("账户密码错误!");
}
return new SimpleAuthenticationInfo(jwt,token,getName());
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("执行授权...");
JwtToken jwtToken= (JwtToken) principals.getPrimaryPrincipal();
User user=userService.selectUserByUsername(jwtConfig.getUsername(jwtToken.getToken()));
// 角色集合
List strRoleList=new ArrayList<>();
// 权限集合
List strPerList=new ArrayList<>();
// 获取用户的角色集合
List roles=user.getRoles();
// 遍历形成角色权限集合
for (Role role : roles) {
strRoleList.add(role.getName());
for (Permission permission : role.getPermissions()) {
if (permission!=null){
strPerList.add(permission.getName());
}
}
}
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.addRoles(strRoleList);
info.addStringPermissions(strPerList);
return info;
}
本人猜想是因为路径拦截的问题(前面的拦截器将路径全部拦截了,后面才不生效),但是苦于找不到源码切入点,于是做了一个试验在ShrioConfig中使用JwtFilter对/a/b路径进行拦截
如下图:
已经拦截成功说明拦截器生效。
因为getSubject(request, response).login(jwtToken);的执行由原来的Controller层迁移到了JwtFilter中所以单独复制了一个生成token的接口目的在于对当前用户执行.login(token)操作,如下图:login2()方法
/**
* 登录接口
* @return JsonData
*/
@PostMapping("token")
public JsonData login(@RequestBody UserQuery userQuery) {
String token= jwtConfig.generateJwt(userQuery.getName(),userQuery.getPwd());
if(token==null){
return JsonData.buildError(-1,"用户名或密码不能为空");
}
return JsonData.buildSuccess(1, token,"操作成功");
}
@PostMapping("token2")
public JsonData login2(@RequestBody UserQuery userQuery) {
Subject subject= SecurityUtils.getSubject();
String token= jwtConfig.generateJwt(userQuery.getName(),userQuery.getPwd());
if(token==null){
return JsonData.buildError(-1,"用户名或密码不能为空");
}
// 即将进入doGetAuthenticationInfo
subject.login(new JwtToken(token));
return JsonData.buildSuccess(1, token,"登录成功");
}
访问login2对应接口,成功返回token,如下图:
接着再次访问需要roles[root]角色的接口,如下图:
从图中可以看出RolesFilter生效
在ShiroConfig中手动加入
@Bean
public RolesFilter getRolesFilter(){
return new RolesFilter();
}
解决思路是在偶尔间看到的一篇博文,地址如下