这段时间准备写一个前后分离的项目,而前后分离免不了涉及到跨域认证,就想到了JWT(JSON Web Token),JWT又是目前比较流行的跨域认证解决方案,再配合上Shiro的权限管理,可以说完美的解决了我的这个问题,废话不多说,Shiro大家都知道,而JWT是什么呢?可以去参考下边这篇文章了解下
JSON Web Token 入门教程-阮一峰
了解完什么是JSON Web Token后,我们来用Jfinal+Shiro+JWT来简单的写个小Demo
代码地址:Jfinal-shiro-jwt
首先是依赖文件,主要依托于以下三个JAR包:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
主要添加下Shiro的过滤器拦截器
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>shiro</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiro</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
创建自定义的ShiroDbRealm继承Shiro的AuthorizingRealm,注释已经写的很清楚了:
package com.perfree.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import com.perfree.jwt.JWTToken;
import com.perfree.jwt.JwtUtils;
import com.perfree.model.User;
public class ShiroDbRealm extends AuthorizingRealm{
/**
* 重写shiro的token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 角色,权限认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//String username = JwtUtils.getUsername(principals.toString());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//这里可以连接数据库根据用户账户进行查询用户角色权限等信息,为简便,直接set
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("all");
return simpleAuthorizationInfo;
}
/**
* 自定义认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
String userName = JwtUtils.getUsername(token);
if (userName == null || userName == "") {
throw new AuthenticationException("token 校验失败");
}
//根据解密的token得到用户名到数据库查询(为省事,直接设置)
User user = new User();
user.setName(userName);
if(user.getName() == null) {
throw new AuthenticationException("用户不存在");
}
if(JwtUtils.verifyJwt(token, userName) == null) {
throw new AuthenticationException("用户名或者密码错误");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
}
由于我们用的jfinal框架,并非spring系列,所以我们要重写下Shiro的拦截器,并配合Jfinal的拦截器来实现,这部分可以参考网上Jfinal整合Shiro的案例:
package com.perfree.shiro;
import java.lang.reflect.Method;
import org.apache.shiro.aop.MethodInvocation;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor;
import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.LogKit;
public class ShiroInterceptor extends AnnotationsAuthorizingMethodInterceptor implements Interceptor {
public ShiroInterceptor() {
getMethodInterceptors();
}
public void intercept(final Invocation inv) {
try {
invoke(new MethodInvocation() {
public Object proceed() throws Throwable {
inv.invoke();
return inv.getReturnValue();
}
public Method getMethod() {
return inv.getMethod();
}
public Object[] getArguments() {
return inv.getArgs();
}
public Object getThis() {
return inv.getController();
}
});
} catch (Throwable e) {
if (e instanceof AuthorizationException) {
doProcessuUnauthorization(inv.getController());
}
LogKit.warn("权限错误:", e);
}
}
/**
* 未授权处理
*
* @param controller controller
*/
private void doProcessuUnauthorization(Controller controller) {
controller.redirect("/login");
}
}
在Jfianl的config配置类中添加我们写的Shiro拦截器
@Override
public void configInterceptor(Interceptors me) {
me.add(new ShiroInterceptor());
}
到这里配合上Shiro的ini配置文件基本就完成了Shiro和Jfinal的整合,接下来书写JWT部分,ini文件我会在JWT部分完成后再添加
主要用来生成及验证
package com.perfree.jwt;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.jfinal.kit.PropKit;
public class JwtUtils {
/**
* 生成jwt
* @return
*/
public static String createJwt(Map<String, String> claims,Date expireDatePoint){
try {
//使用HMAC256进行加密
Algorithm algorithm = Algorithm.HMAC256(PropKit.get("jwt.secretkey"));
//创建jwt
JWTCreator.Builder builder = JWT.create().withIssuer(PropKit.get("jwt.issuer")).withExpiresAt(expireDatePoint);
//传入参数
claims.forEach((key,value)-> {
builder.withClaim(key, value);
});
//签名加密
return builder.sign(algorithm);
} catch (IllegalArgumentException e) {
return "";
} catch (UnsupportedEncodingException e) {
return "";
}
}
/**
* 验证jwt
* @return
*/
public static Map<String, String> verifyJwt(String token,String userName) {
Algorithm algorithm = null;
Map<String, String> resultMap = new HashMap<>();
try {
//使用HMAC256进行加密
algorithm = Algorithm.HMAC256(PropKit.get("jwt.secretkey"));
//解密
JWTVerifier verifier = JWT.require(algorithm).withIssuer(PropKit.get("jwt.issuer")).withClaim("name", userName).build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> map = jwt.getClaims();
map.forEach((k,v) -> resultMap.put(k, v.asString()));
} catch (Exception e) {
return null;
}
return resultMap;
}
/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("name").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
这个就没啥说的了
package com.perfree.jwt;
import org.apache.shiro.authc.AuthenticationToken;
public class JWTToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// 密钥
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
接着自定义一个Shiro的过滤器,主要用来验证Token,看用户是否想要登录,这里我们的Token都是存在Header的authc字段里,如Header里包含authc字段,我们就只需要验证Token就行了,如不包含,我们给他重定向至Login页面
package com.perfree.jwt;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
/**
* 自定义Shiro的过滤器
* @author Perfree
*
*/
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* 判断用户是否想要登入。
* 检测header里面是否包含authc字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("authc");
return authorization != null;
}
/**
* 如果携带token进行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response){
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("authc");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletResponse resp = (HttpServletResponse)response;
Boolean flag = true;
//判断用户是否携带了token
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
flag = false;
}
if(!flag) {
try {
resp.sendRedirect("/login");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return flag;
}else {
//未携带token,重定向至登录页面
try {
resp.sendRedirect("/login");
} catch (IOException e1) {
}
return false;
}
}
}
主要配置下jwt的秘钥及授权方
#秘钥
jwt.secretkey=qwdjkshdksfgkdsfhsfds4f56d7s8f65s4d6ad45a4sd56
#授权方
jwt.issuer=perfree
[main]
#realm 自定 义 realm
shiroDbRealm=com.perfree.shiro.ShiroDbRealm
securityManager.realms = $shiroDbRealm
sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
securityManager.sessionManager=$sessionManager
securityManager.sessionManager.sessionValidationSchedulerEnabled = false
# 退出跳转路径
logout.redirectUrl = /login
[filters]
app_authc = com.perfree.jwt.JWTFilter
app_authc.loginUrl = /login
# 登录成功跳转路径 可以自己定义
app_authc.successUrl = /index
#路径角色权限设置
[urls]
/login = anon
/doLogin = anon
/resources/** = anon
/logout = logout
/** = app_authc,roles[admin]
这里就不写页面了,直接返回字符串进行简单的测试就好了
package com.perfree.controller;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.jfinal.core.Controller;
import com.perfree.common.AjaxResult;
import com.perfree.jwt.JwtUtils;
/**
* 测试Controller
* @author Perfree
*/
public class TestController extends Controller{
/**
* 首页
*/
public void index() {
renderText("这是首页");
}
/**
* 登录页
*/
public void login() {
renderText("请登录");
}
/**
* 登录操作
*/
public void doLogin() {
try {
String name = getPara("name");
String password = getPara("password");
if(name.equals("perfree") && password.equals("123456")) {
Map<String,String> map = new HashMap<>();
map.put("name", name);
renderJson(new AjaxResult(AjaxResult.SUCCESS, JwtUtils.createJwt(map, new Date(System.currentTimeMillis()+360000))));
}else {
renderJson(new AjaxResult(AjaxResult.ERROR,"用户名或密码错误"));
}
} catch (Exception e) {
renderJson(new AjaxResult(AjaxResult.FAILD,"系统异常"));
}
}
}
这里直接调用controller进行测试就好了,首先我们访问http://127.0.0.1:8088就会看到返回结果是请登录,也就是重定向至了登录页,接下来我们模拟下登录,登录之后给我们返回了token,如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwZXJmcmVlIiwibmFtZSI6InBlcmZyZWUiLCJleHAiOjE1NTEyNTA0NzR9.L-V_3DjIuSEbW2jFPIzvUbIsXD9Z3-lzoAji6MbaE78
接着我们再请求http://127.0.0.1:8088 ,这次带上token,在header中携带登录时返回的token再去访问,成功看到了首页,到这里,简单的Demo就算完成了,代码写的不怎么样,如有错误,欢迎指出,共同交流