Perfree

Jfinal+Shiro+Jwt权限认证简单Demo
这段时间准备写一个前后分离的项目,而前后分离免不了涉及到跨域认证,就想到了JWT(JSON Web Token),...
扫描右侧二维码阅读全文
27
2019/02

Jfinal+Shiro+Jwt权限认证简单Demo

这段时间准备写一个前后分离的项目,而前后分离免不了涉及到跨域认证,就想到了JWT(JSON Web Token),JWT又是目前比较流行的跨域认证解决方案,再配合上Shiro的权限管理,可以说完美的解决了我的这个问题,废话不多说,Shiro大家都知道,而JWT是什么呢?可以去参考下边这篇文章了解下
JSON Web Token 入门教程-阮一峰
了解完什么是JSON Web Token后,我们来用Jfinal+Shiro+JWT来简单的写个小Demo
代码地址:Jfinal-shiro-jwt

pom依赖

首先是依赖文件,主要依托于以下三个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>

web.xml

主要添加下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>

重写ShiroRealm

创建自定义的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());
    }
}

Shiro拦截器

由于我们用的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");
    }
}

添加Shiro拦截器

在Jfianl的config配置类中添加我们写的Shiro拦截器

@Override
public void configInterceptor(Interceptors me) {
    me.add(new ShiroInterceptor());
}

到这里配合上Shiro的ini配置文件基本就完成了Shiro和Jfinal的整合,接下来书写JWT部分,ini文件我会在JWT部分完成后再添加

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;
        }
    }
}

JWTToken实体类

这个就没啥说的了

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过滤器

接着自定义一个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;
        }
    }
}

config.properties配置文件

主要配置下jwt的秘钥及授权方

#秘钥
jwt.secretkey=qwdjkshdksfgkdsfhsfds4f56d7s8f65s4d6ad45a4sd56
#授权方
jwt.issuer=perfree

Shiro配置文件ini

[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]

Controller

这里就不写页面了,直接返回字符串进行简单的测试就好了

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就算完成了,代码写的不怎么样,如有错误,欢迎指出,共同交流

Last modification:April 14th, 2019 at 11:12 am
If you think my article is useful to you, please feel free to appreciate

One comment

  1. Bobby

    我直接运行demo,直接可以访问首页啊,都不会跳去请登录

Leave a Comment