Shiro整合JWT+Token过期刷新,demo,详解

2019-09-26 21:46:36

参考资料 Shiro整合JWT+Token过期刷新,全都帮你整好了


最近使用SpringBoot集成Shiro,JWT快速搭建了一个后台系统,Shiro前面已经使用过,JWT(JSON Web Tokens)是一种用于安全的传递信息而采用的一种标准。Web系统中,我们使用加密的Json来生成Token在服务端与客户端无状态传输,代替了之前常用的Session。
系统采用Redis作为缓存,解决Token过期更新的问题,同时集成SSO登录,完整过程这里来总结一下。

0. JWT登录主要流程:

  1. 登录时,密码验证通过,取当前时间戳生成签名Token,放在Response Header的Authorization属性中,同时在缓存中记录值为当前时间戳的RefreshToken,并设置有效期。

  2. 客户端请求每次携带Token进行请求。

  3. 服务端每次校验请求的Token有效后,同时比对Token中的时间戳与缓存中的RefreshToken时间戳是否一致,一致则判定Token有效。

  4. 当请求的Token被验证时抛出TokenExpiredException异常时说明Token过期,校验时间戳一致后重新生成Token并调用登录方法。

  5. 每次生成新的Token后,同时要根据新的时间戳更新缓存中的RefreshToken,以保证两者时间戳一致。


1. Shiro配置

首先是Shiro的配置,定义两个类ShiroChonfig以及ShiroRealm用来配置Shiro,以及验证部分。
这里重要的是关闭Session,因为我们使用JWT来传输安全信息。自定义缓存管理器,同时我们要添加一个JwttFilter,将所有的请求交由它处理。

示例代码:

@Configuration

public class ShiroConfig   {

    @Bean

    public   LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {

        return   new LifecycleBeanPostProcessor();

    }

 

    @Bean

    @DependsOn("lifecycleBeanPostProcessor")

    public   static DefaultAdvisorAutoProxyCreator getLifecycleBeanPostProcessor() {

        DefaultAdvisorAutoProxyCreator   defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();

        //   强制使用cglib

        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

        return   defaultAdvisorAutoProxyCreator;

    }

 

    @Bean

    public   AuthorizationAttributeSourceAdvisor   authorizationAttributeSourceAdvisor(DefaultWebSecurityManager   securityManager) {

        AuthorizationAttributeSourceAdvisor   advisor = new AuthorizationAttributeSourceAdvisor();

        advisor.setSecurityManager(securityManager);

        return   advisor;

    }

 

    @Bean

    public   DefaultWebSecurityManager  securityManager(ShiroRealm   shiroRealm,ShiroCacheManager shiroCacheManager){

        DefaultWebSecurityManager   securityManager =  new DefaultWebSecurityManager();

 

        securityManager.setRealm(shiroRealm);

 

        //关闭shiro自带的session

        DefaultSubjectDAO   subjectDAO = new DefaultSubjectDAO();

        DefaultSessionStorageEvaluator   defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();

        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);

        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

        securityManager.setSubjectDAO(subjectDAO);

 

        //自定义缓存管理

        securityManager.setCacheManager(shiroCacheManager);

        return   securityManager;

    }

 

    @Bean

    public   ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager)   {

        ShiroFilterFactoryBean   shiroFilter = new ShiroFilterFactoryBean();

        shiroFilter.setSecurityManager(securityManager);

 

        //   添加jwt过滤器

        Map<String,   Filter> filterMap = new HashMap<>();

        filterMap.put("jwt",   jwtFilter());

        filterMap.put("logout", new   SystemLogoutFilter());

        shiroFilter.setFilters(filterMap);

 

        //拦截器

        Map<String,String>   filterRuleMap = new LinkedHashMap<>();

        filterRuleMap.put("/logout", "logout");

        filterRuleMap.put("/**", "jwt");

        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

 

        return   shiroFilter;

    }

 

    @Bean

    public   JwtFilter jwtFilter(){

        return   new JwtFilter();此处为AccessToken

    }

}

用户验证以及权限验证的地方,用户验证多加了一个校验,就是我们当前请求的token中包含的时间戳与缓存中的RefreshToken对比,一致才验证通过。

示例代码:

@Service

public class ShiroRealm extends   AuthorizingRealm {

    @Autowired

    private   IBpUserService userService;

    @Autowired

    private   IBpRoleService roleService;

    @Autowired

    private   IBpAuthorityService bpAuthorityService;

    @Autowired

    private   CacheClient cacheClient;

     

    @Override

    public   boolean supports(AuthenticationToken token) {

        return   token instanceof JwtToken;

    }

 

    /**

     *   用户名信息验证

     *   @param auth

     *   @return

     *   @throws AuthenticationException

     */

    @Override

    protected   AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth)

            throws   AuthenticationException {

        String   token = (String)auth.getPrincipal();

        String   account  = JwtUtil.getClaim(token,SecurityConsts.ACCOUNT);

 

        if   (account == null) {

            throw   new AuthenticationException("token invalid");

        }

 

        BpUser   bpUserInfo = userService.findUserByAccount(account);

        if   (bpUserInfo == null) {

            throw   new AuthenticationException("BpUser didn't existed!");

        }

 

        String   refreshTokenCacheKey = SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;

        if   (JwtUtil.verify(token) && cacheClient.exists(refreshTokenCacheKey)) {

            String   currentTimeMillisRedis = cacheClient.get(refreshTokenCacheKey);

            //   获取AccessToken时间戳,与RefreshToken的时间戳对比

            if   (JwtUtil.getClaim(token,   SecurityConsts.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {

                return   new SimpleAuthenticationInfo(token, token, "shiroRealm");

            }

        }

        throw   new AuthenticationException("Token expired or incorrect.");

    }

 

    /**

     *   检查用户权限

     *   @param principals

     *   @return

     */

    @Override

    protected   AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        SimpleAuthorizationInfo   authorizationInfo = new SimpleAuthorizationInfo();

 

        String   account = JwtUtil.getClaim(principals.toString(), SecurityConsts.ACCOUNT);

        BpUser   bpUserInfo = userService.findUserByAccount(account);

 

        //获取用户角色

        List<BpRole>   bpRoleList = roleService.findRoleByUserId(bpUserInfo.getId());

        //获取权限

        List<Object>   bpAuthorityList = bpAuthorityService.findByUserId(bpUserInfo.getId());

        for(BpRole   bpRole : bpRoleList){

            authorizationInfo.addRole(bpRole.getName());

            for(Object   auth: bpAuthorityList){

                authorizationInfo.addStringPermission(auth.toString());

            }

        }

        return   authorizationInfo;

    }

}

这里我们定义了一些常量,其中有请求头包含的Token的属性,以及放入缓存中的Key

示例代码:

public class SecurityConsts   {

    public   static final String LOGIN_SALT = "storyweb-bp";

    //request请求头属性

    public   static final String REQUEST_AUTH_HEADER="Authorization";

 

    //JWT-account

    public   static final String ACCOUNT = "account";

 

    //Shiro   redis 前缀

    public   static final String PREFIX_SHIRO_CACHE = "storyweb-bp:cache:";

 

    //redis-key-前缀-shiro:refresh_token

    public   final static String PREFIX_SHIRO_REFRESH_TOKEN   = "storyweb-bp:refresh_token:";

 

    //JWT-currentTimeMillis

    public   final static String CURRENT_TIME_MILLIS = "currentTimeMillis";

}


2. JWT 配置

这里我们有几个参数放在配置文件中:

示例代码:

token:

  #    token过期时间,单位分钟

  tokenExpireTime:   120

  #    RefreshToken过期时间,单位:分钟, 24*60=1440

  refreshTokenExpireTime:   1440

  #    shiro缓存有效期,单位分钟,2*60=120

  shiroCacheExpireTime:   120

  #    token加密密钥

  secretKey:   storywebkey

 

示例代码:

@ConfigurationProperties(prefix   = "token")

@Data

public class JwtProperties   {

    //token过期时间,单位分钟

    Integer   tokenExpireTime;

    //刷新Token过期时间,单位分钟

    Integer   refreshTokenExpireTime;

    //Shiro缓存有效期,单位分钟

    Integer   shiroCacheExpireTime;

    //token加密密钥

    String   secretKey;

}

当然了,你需要在SpringBoot的Application启动类中,加入注解:
@EnableConfigurationProperties({JwtProperties.class})

示例代码:

public class JwtToken implements   AuthenticationToken {

    //密钥

    private   String token;

 

    public   JwtToken(String token) {

        this.token   = token;

    }

 

    @Override

    public   Object getPrincipal() {

        return   token;

    }

 

    @Override

    public   Object getCredentials() {

        return   token;

    }

}

接下来是Jwt的Fiter,集成自Shiro的BasicHttpAuthenticationFilter,这里的注释比较详细。

示例代码:

public class JwtFilter extends   BasicHttpAuthenticationFilter {

 

    private   Logger LOGGER = LoggerFactory.getLogger(this.getClass());

 

    @Autowired

    CacheClient   cacheClient;

    @Autowired

    JwtProperties   jwtProperties;

 

    /**

     *   检测Header里Authorization字段

     *   判断是否登录

     */

    @Override

    protected   boolean isLoginAttempt(ServletRequest request, ServletResponse response) {

        HttpServletRequest   req = (HttpServletRequest) request;

        String   authorization = req.getHeader(SecurityConsts.REQUEST_AUTH_HEADER);

        return   authorization != null;

    }

 

    /**

     *   登录验证

     *   @param request

     *   @param response

     *   @return

     *   @throws Exception

     */

    @Override

    protected   boolean executeLogin(ServletRequest request, ServletResponse   response) throws Exception {

        HttpServletRequest   httpServletRequest = (HttpServletRequest) request;

        String   authorization =   httpServletRequest.getHeader(SecurityConsts.REQUEST_AUTH_HEADER);

 

        JwtToken   token = new JwtToken(authorization);

        //   提交给realm进行登入,如果错误他会抛出异常并被捕获

        getSubject(request,   response).login(token);

 

        //   绑定上下文

        String   account = JwtUtil.getClaim(authorization, SecurityConsts.ACCOUNT);

        UserContext   userContext= new UserContext(new LoginUser(account));

 

        //   如果没有抛出异常则代表登入成功,返回true

        return   true;

    }

 

    /**

     *   刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问

     */

    private   boolean refreshToken(ServletRequest request, ServletResponse response) {

        //   获取AccessToken(Shiro中getAuthzHeader方法已经实现)

        String   token = this.getAuthzHeader(request);

        //   获取当前Token的帐号信息

        String   account = JwtUtil.getClaim(token, SecurityConsts.ACCOUNT);

        String   refreshTokenCacheKey = SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;

        //   判断Redis中RefreshToken是否存在

        if   (cacheClient.exists(refreshTokenCacheKey)) {

            //   获取RefreshToken时间戳,及AccessToken中的时间戳

            //   相比如果一致,进行AccessToken刷新

            String   currentTimeMillisRedis = cacheClient.get(refreshTokenCacheKey);

            String   tokenMillis=JwtUtil.getClaim(token, SecurityConsts.CURRENT_TIME_MILLIS);

 

            if   (tokenMillis.equals(currentTimeMillisRedis)) {

 

                //   设置RefreshToken中的时间戳为当前最新时间戳

                String   currentTimeMillis = String.valueOf(System.currentTimeMillis());

                Integer   refreshTokenExpireTime = jwtProperties.refreshTokenExpireTime;

                cacheClient.set(refreshTokenCacheKey,   currentTimeMillis,refreshTokenExpireTime*60l);

 

                //   刷新AccessToken,为当前最新时间戳

                token   = JwtUtil.sign(account, currentTimeMillis);

 

                //   使用AccessToken 再次提交给ShiroRealm进行认证,如果没有抛出异常则登入成功,返回true

                JwtToken   jwtToken = new JwtToken(token);

                this.getSubject(request,   response).login(jwtToken);

 

                //   设置响应的Header头新Token

                HttpServletResponse   httpServletResponse = (HttpServletResponse) response;

                httpServletResponse.setHeader(SecurityConsts.REQUEST_AUTH_HEADER,   token);

                httpServletResponse.setHeader("Access-Control-Expose-Headers",   SecurityConsts.REQUEST_AUTH_HEADER);

                return   true;

            }

        }

        return   false;

    }

 

    /**

     *   是否允许访问

     *   @param request

     *   @param response

     *   @param mappedValue

     *   @return

     */

    @Override

    protected   boolean isAccessAllowed(ServletRequest request, ServletResponse response,   Object mappedValue) {

        if   (isLoginAttempt(request, response)) {

            try   {

                this.executeLogin(request,   response);

            } catch   (Exception e) {

                String   msg = e.getMessage();

                Throwable   throwable = e.getCause();

                if   (throwable != null && throwable instanceof SignatureVerificationException)   {

                    msg   = "Token或者密钥不正确("   + throwable.getMessage() + ")";

                } else   if (throwable != null && throwable instanceof TokenExpiredException)   {

                    //   AccessToken已过期

                    if   (this.refreshToken(request, response)) {

                        return   true;

                    } else   {

                        msg   = "Token已过期(" +   throwable.getMessage() + ")";

                    }

                } else   {

                    if   (throwable != null) {

                        msg   = throwable.getMessage();

                    }

                }

                this.response401(request,   response, msg);

                return   false;

            }

        }

        return   true;

    }

 

    /**

     *   401非法请求

     *   @param req

     *   @param resp

     */

    private   void response401(ServletRequest req, ServletResponse resp,String msg) {

        HttpServletResponse   httpServletResponse = (HttpServletResponse) resp;

        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());

        httpServletResponse.setCharacterEncoding("UTF-8");

        httpServletResponse.setContentType("application/json;   charset=utf-8");

        PrintWriter   out = null;

        try   {

            out   = httpServletResponse.getWriter();

 

            Result   result = new Result();

            result.setResult(false);

            result.setCode(Constants.PASSWORD_CHECK_INVALID);

            result.setMessage(msg);

            out.append(JSON.toJSONString(result));

        } catch   (IOException e) {

            LOGGER.error("返回Response信息出现IOException异常:" + e.getMessage());

        } finally   {

            if   (out != null) {

                out.close();

            }

        }

    }

}

这里再重复一下:当请求验证Token时抛出TokenExpiredException异常后,校验缓存中的RefreshToken的时间戳是否与当前请求Token时间戳一致,倘若一致,则重新生成Token,以当前时间戳更新缓存中的RefreshToken时间戳;倘若不一致,则以Json格式直接响应401未登录错误。
采用前后端分离的方式,我们的401就需要直接返回JSON格式的响应。

示例代码:

@Component

public class JwtUtil   {

 

    @Autowired

    JwtProperties   jwtProperties;

 

    @Autowired

    private   static JwtUtil jwtUtil;

 

    @PostConstruct

    public   void init() {

        jwtUtil   = this;

        jwtUtil.jwtProperties   = this.jwtProperties;

    }

 

    /**

     *   校验token是否正确

     *   @param token

     *   @return

     */

    public   static boolean verify(String token) {

        String   secret = getClaim(token, SecurityConsts.ACCOUNT) +   jwtUtil.jwtProperties.secretKey;

        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;

        }

    }

 

    /**

     *   生成签名,5min后过期

     *   @param account

     *   @param currentTimeMillis

     *   @return

     */

    public   static String sign(String account, String currentTimeMillis) {

        //   帐号加JWT私钥加密

        String   secret = account + jwtUtil.jwtProperties.getSecretKey();

        //   此处过期时间,单位:毫秒

        Date   date = new Date(System.currentTimeMillis() +   jwtUtil.jwtProperties.getTokenExpireTime()*60*1000l);

        Algorithm   algorithm = Algorithm.HMAC256(secret);

 

        return   JWT.create()

                .withClaim(SecurityConsts.ACCOUNT,   account)

                .withClaim(SecurityConsts.CURRENT_TIME_MILLIS,   currentTimeMillis)

                .withExpiresAt(date)

                .sign(algorithm);

    }

}


3. 绑定当前上下文用户

用户登录后,在业务里想要获取当前登录用户信息,一是可以在登录时缓存用户信息,二是少量信息从token里拿,这里当每次验证请求成功后,我们都将当前用户信息绑定到当前的上下文中,这里我只提取了账号。

示例代码:

@Data

public class LoginUser implements   Serializable {

    private   static final long serialVersionUID = 1L;

 

    public   Long userId;          // 主键ID

    public   String account;      // 账号

    public   String name;         // 姓名

 

    public   LoginUser() {

    }

 

    public   LoginUser(String account) {

        this.account=account;

    }

 

    public   LoginUser(Long userId, String account, String name) {

        this.userId   = userId;

        this.account   = account;

        this.name   = name;

    }

}

 

 

public class UserContext implements   AutoCloseable {

    static   final ThreadLocal<LoginUser> current = new ThreadLocal<>();

 

    public   UserContext(LoginUser user) {

        current.set(user);

    }

 

    public   static LoginUser getCurrentUser() {

        return   current.get();

    }

 

    public   void close() {

        current.remove();

    }

}


4. 缓存

缓存这里的实现,可以自己完善,这里只实现了部分的方法。

示例代码:

@Service

public class ShiroCacheManager implements   CacheManager {

    @Autowired

    CacheClient   cacheClient;

 

    @Override

    public   <K, V> Cache<K, V> getCache(String s) throws CacheException   {

        return   new ShiroCache<K,V>(cacheClient);

    }

}

 

/**

 * 重写Shiro的Cache保存读取

 * @param   <K>

 * @param   <V>

 */

public class ShiroCache<K,V> implements   Cache<K,V> {

 

    private   CacheClient cacheClient;

 

    public   ShiroCache(CacheClient cacheClient) {

        this.cacheClient   = cacheClient;

    }

 

    /**

     *   获取缓存

     *   @param key

     *   @return

     *   @throws CacheException

     */

    @Override

    public   Object get(Object key) throws CacheException {

        String   tempKey= this.getKey(key);

        if(cacheClient.exists(tempKey)){

            return   cacheClient.getObject(tempKey);

        }

        return   null;

    }

 

    /**

     *   保存缓存

     *   @param key

     *   @param value

     *   @return

     *   @throws CacheException

     */

    @Override

    public   Object put(Object key, Object value) throws CacheException {

        return   cacheClient.setObject(this.getKey(key), value);

    }

 

    /**

     *   移除缓存

     *   @param key

     *   @return

     *   @throws CacheException

     */

    @Override

    public   Object remove(Object key) throws CacheException {

        String   tempKey= this.getKey(key);

        if(cacheClient.exists(tempKey)){

            cacheClient.del(tempKey);

        }

        return   null;

    }

 

    @Override

    public   void clear() throws CacheException {}

 

    @Override

    public   int size() {

        //@TODO

        return   20;

    }

 

    @Override

    public   Set<K> keys() {

        return   null;

    }

 

    @Override

    public   Collection<V> values() {

        Set   keys = this.keys();

        List<V>   values = new ArrayList<>();

        for   (Object key : keys) {

            values.add((V)cacheClient.getObject(this.getKey(key)));

        }

        return   values;

    }

 

    /**

     *   根据名称获取

     *   @param key

     *   @return

     */

    private   String getKey(Object key) {

        return   SecurityConsts.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(),   SecurityConsts.ACCOUNT);

    }

}

 

示例代码:

//shiro工具类

public class ShiroKit   {

    public   final static String hashAlgorithmName = "MD5";

 

    //循环次数

    public   final static int hashIterations = 1024;

 

    /**

     *   shiro密码加密工具类

     *

     *   @param credentials 密码

     *   @param saltSource 密码盐

     *   @return

     */

    public   static String md5(String credentials, String saltSource) {

        ByteSource   salt = new Md5Hash(saltSource);

        return   new SimpleHash(hashAlgorithmName, credentials, salt,   hashIterations).toString();

    }

}

5. 登录

示例代码:

@Controller

@RequestMapping(value="/user")

public class LoginController   {

 

    @Autowired

    IBpUserService   bpUserService;

 

    /**

     *   登录

     *   @param user

     *   @return

     */

    @SuppressWarnings("unchecked")

    @RequestMapping(value="/login")

    @ResponseBody

    public   Result login(HttpServletResponse response,@RequestBody User user) {

        return   bpUserService.login(user,response);

    }

}

 

//Service

@Service

public class BpUserServiceImpl extends   ServiceImpl<BpUserMapper, BpUser> implements IBpUserService {

    @Autowired

    CacheClient   CacheClient;

 

     /**

     *   用户登录

     *   @param user

     *   @return

     */

    @Override

    public   Result login(User user, HttpServletResponse response) {

        Assert.notNull(user.getUsername(), "用户名不能为空");

        Assert.notNull(user.getPassword(), "密码不能为空");

 

        BpUser   userBean = this.findUserByAccount(user.getUsername());

 

        if(userBean==null){

            return   new Result(false, "用户不存在", null,   Constants.PASSWORD_CHECK_INVALID);

        }

 

        //域账号直接提示账号不存在

        if   ("1".equals(userBean.getDomainFlag())) {

            return   new Result(false, "账号不存在", null,   Constants.PASSWORD_CHECK_INVALID);

        }

 

        String   encodePassword = ShiroKit.md5(user.getPassword(), SecurityConsts.LOGIN_SALT);

        if   (!encodePassword.equals(userBean.getPassword())) {

            return   new Result(false, "用户名或密码错误", null,   Constants.PASSWORD_CHECK_INVALID);

        }

 

        //账号是否锁定

        if   ("0".equals(userBean.getStatus())) {

            return   new Result(false, "该账号已被锁定", null,   Constants.PASSWORD_CHECK_INVALID);

        }

 

        //验证成功后处理

        this.loginSuccess(userBean.getAccount(),response);

 

        //登录成功

        return   new Result(true, "登录成功", null   ,Constants.TOKEN_CHECK_SUCCESS);

    }

 

    /**

     *   登录后更新缓存,生成token,设置响应头部信息

     *   @param account

     *   @param response

     */

    private   void loginSuccess(String account, HttpServletResponse response){

 

        String   currentTimeMillis = String.valueOf(System.currentTimeMillis());

 

        //   清除可能存在的Shiro权限信息缓存

        String   tokenKey=SecurityConsts.PREFIX_SHIRO_CACHE + account;

        if   (cacheClient.exists(tokenKey)) {

            cacheClient.del(tokenKey);

        }

 

        //更新RefreshToken缓存的时间戳

        String   refreshTokenKey= SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;

        if   (cacheClient.exists(refreshTokenKey)) {

            cacheClient.set(refreshTokenKey,   currentTimeMillis, jwtProperties.getRefreshTokenExpireTime()*60*60l);

        }else{

            cacheClient.set(refreshTokenKey,   currentTimeMillis, jwtProperties.getRefreshTokenExpireTime()*60*60l);

        }

 

        //生成token

        JSONObject   json = new JSONObject();

        String   token = JwtUtil.sign(account, currentTimeMillis);

        json.put("token",token   );

 

        //写入header

        response.setHeader(SecurityConsts.REQUEST_AUTH_HEADER,   token);

        response.setHeader("Access-Control-Expose-Headers",   SecurityConsts.REQUEST_AUTH_HEADER);

    }

}

登录成功后,我们在生成Token的同时,将当前时间戳以RefreshToken为Key存入Redis,用于Token过期时的校验及刷新。

当我们在业务中需要访问上下文用户时,可以这样获取:
UserContext.getCurrentUser().getAccount()


6. 注销登录状态

采用前后端分离的方式,当用户注销后,后端依然是以Json方式返回,因此,我们通过过滤器处理请求,注销完成返回Json结果。
再前面,我们已经添加了自定义的过滤器
SystemLogoutFilter到Shiro的ShiroFilterFactoryBean中,这里只要实现就可以了。

示例代码:

public class SystemLogoutFilter extends   LogoutFilter {

    private   static final Logger logger =   LoggerFactory.getLogger(SystemLogoutFilter.class);

 

    @Override

    protected   boolean preHandle(ServletRequest request, ServletResponse   response) throws Exception {

        Subject   subject = getSubject(request, response);

        try   {

            subject.logout();

        } catch   (Exception ex) {

            logger.error("退出登录错误",ex);

        }

 

        this.writeResult(response);

        //不执行后续的过滤器

        return   false;

    }

 

    private   void writeResult(ServletResponse response){

        //响应Json结果

        PrintWriter   out = null;

        try   {

            out   = response.getWriter();

            Result   result = new Result(true,null,null,Constants.TOKEN_CHECK_SUCCESS);

            out.append(JSON.toJSONString(result));

        } catch   (IOException e) {

            logger.error("返回Response信息出现IOException异常:" + e.getMessage());

        } finally   {

            if   (out != null) {

                out.close();

            }

        }

    }

}


7. 添加依赖

把依赖放到最后,因为这个不需要说。

示例代码:

<dependency>

    <groupId>org.apache.shiro</groupId>

    <artifactId>shiro-spring</artifactId>

    <version>1.4.0</version>

</dependency>

<dependency>

    <groupId>org.apache.shiro</groupId>

    <artifactId>shiro-ehcache</artifactId>

    <version>1.4.0</version>

</dependency>

<!--JWT-->

<dependency>

    <groupId>com.auth0</groupId>

    <artifactId>java-jwt</artifactId>

    <version>3.4.1</version>

</dependency>

<!--Redis-->

<dependency>

    <groupId>redis.clients</groupId>

    <artifactId>jedis</artifactId>

    <version>2.9.0</version>

</dependency>


后续补充

  1. 关于本篇中Token刷新方案做了一些修改,详见 采用JWT有效期内刷新Token方案,解决并发请求问题

  2. 问的朋友比较多,于是就将项目后端代码上传至GitHub,地址:https://github.com/sunnj/story-admin

  3. 本项目的前端仓库地址:https://github.com/sunnj/story-admin-console


参考资料

  1. JSON Web Token 入门教程

  2. Shiro+JWT+Spring      Boot Restful简易教程

  3. SpringBoot + Shiro + JWT集成Redis缓存(Jedis)

 


  • 2019-09-26 19:03:33

    spring post jackson的反序列化需要无参构造函数

    JSON parse error: Cannot construct instance of `com.**` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.**` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)  at [Source: (PushbackInputStream); line: 2, column: 2]] ———————————————— 版权声明:本文为CSDN博主「冰夏之夜影」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/u011561335/article/details/91346777

  • 2019-09-26 21:46:36

    Shiro整合JWT+Token过期刷新,demo,详解

    最近使用SpringBoot集成Shiro,JWT快速搭建了一个后台系统,Shiro前面已经使用过,JWT(JSON Web Tokens)是一种用于安全的传递信息而采用的一种标准。Web系统中,我们使用加密的Json来生成Token在服务端与客户端无状态传输,代替了之前常用的Session。 系统采用Redis作为缓存,解决Token过期更新的问题,同时集成SSO登录,完整过程这里来总结一下。

  • 2019-09-26 21:48:15

    解决UEditor超出最大字数后只提示不限制的问题

    最近项目用到百度额UEditor文本编辑器,今天测试向我提出了一个问题。就是在输入的文字超过默认的最大字数限制之后,虽然提示“字数超过最大范围,服务器可能拒绝保存”,但是仍然可以点击保存按钮进行保存。

  • 2019-09-27 14:49:33

    Google Guava介绍和体检

    JDK提供的String还不够好么?也许还不够友好,至少让我们用起来还不够爽,还得操心!举个栗子,比如String提供的split方法,我们得关心空字符串吧,还得考虑返回的结果中存在null元素吧,只提供了前后trim的方法(如果我想对中间元素进行trim呢)。

  • 2019-09-28 00:03:21

    shiro的session共享,持久化

     shiro的session创建与session的查询、更新、过期、删除中,shiro对session的操作基本都讲到了,但还缺一个session共享没有讲解;session共享的原理其实在自定义session管理一文已经讲过了,本文不讲原理,只看看shiro的session共享的实现。