spring boot shiro 无状态token认证

一、spring boot shiro 无状态token认证项目结构图

项目结构图

二、无状态spring boot shiro相关配置

2.1shiro redis 缓存配置

首先是实现shiro的cache接口

$title(RedisCache.java)
package net.xqlee.project.demo.shiro.config.shiro.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * Redis的Shiro缓存对象实现
 * 
 * @author xq
 *
 * @param <K>
 * @param <V>
 */
public class RedisCache<K, V> implements Cache<K, V> {
	private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
	private RedisTemplate<K, V> redisTemplate;
	private final static String PREFIX = "shiro-cache:";
	private String cacheKey;
	private long globExpire = 30;

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public RedisCache(final String name, final RedisTemplate redisTemplate) {
		this.cacheKey = PREFIX + name + ":";
		this.redisTemplate = redisTemplate;
	}

	@Override
	public V get(K key) throws CacheException {
		logger.debug("Shiro从缓存中获取数据KEY值["+key+"]");
		redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MINUTES);
		return redisTemplate.boundValueOps(getCacheKey(key)).get();
	}

	@Override
	public V put(K key, V value) throws CacheException {
		V old = get(key);
		redisTemplate.boundValueOps(getCacheKey(key)).set(value);
		return old;
	}

	@Override
	public V remove(K key) throws CacheException {
		V old = get(key);
		redisTemplate.delete(getCacheKey(key));
		return old;
	}

	@Override
	public void clear() throws CacheException {
		redisTemplate.delete(keys());

	}

	@Override
	public int size() {
		return keys().size();
	}

	@Override
	public Set<K> keys() {
		return redisTemplate.keys(getCacheKey("*"));
	}

	@Override
	public Collection<V> values() {
		Set<K> set = keys();
		List<V> list = new ArrayList<>();
		for (K s : set) {
			list.add(get(s));
		}
		return list;
	}

	@SuppressWarnings("unchecked")
	private K getCacheKey(Object k) {
		return (K) (this.cacheKey + k);
	}
}

接下来是实现shiro的CacheManager

$title(RedisCacheManager.java)
package net.xqlee.project.demo.shiro.config.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * Redis的Shiro缓存管理器实现
 * 
 * @author xq
 *
 */
public class RedisCacheManager implements CacheManager {
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

	@Override
	public <K, V> Cache<K, V> getCache(String name) throws CacheException {
		return new RedisCache<>(name, redisTemplate);
	}

}

shiro的缓存配置就是以上两个文件了。至于redis的配置这里就不讲述了。

2.2自定义一个AuthenticationToken实现

自定义一个AuthenticationToken实现用于登录认证等。

$title(StatelessAuthenticationToken.java)
package net.xqlee.project.demo.shiro.config.shiro;

import org.apache.shiro.authc.AuthenticationToken;

import java.util.Map;

/**
 * 自定义实现Shiro的一个认证Token
 *
 * @author xq
 */
public class StatelessAuthenticationToken implements AuthenticationToken {

    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private String token;// 用户登录后获取的令牌;
    // 构造函数

    public StatelessAuthenticationToken() {
    }

    public StatelessAuthenticationToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return this;
    }

}

这个类其实很简单。就是实现接口并添加了一个token的字段。用于后面进行认证。

2.3 自定义shiro的Realm

$title(UserRealm.java)
package net.xqlee.project.demo.shiro.config.shiro;

import net.xqlee.project.demo.shiro.pojo.LoginAccount;
import net.xqlee.project.demo.shiro.service.UserService;
import org.apache.shiro.authc.*;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 实现一个基于JDBC的Realm,继承AuthorizingRealm可以看见需要重写两个方法,doGetAuthorizationInfo和doGetAuthenticationInfo
 *
 * @author xqlee
 */
@Component
public class UserRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(UserRealm.class);

    /*** 用户业务处理类,用来查询数据库中用户相关信息 ***/
    @Autowired
    UserService userService;

    /**
     * 该Realm仅支持自定义的StatelessAuthenticationToken类型Token,其他类型处理将会抛出异常
     *
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof StatelessAuthenticationToken;
    }

    /***
     * 获取用户授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("##################执行Shiro权限认证##################");
        // 获取用户名
        LoginAccount account = (LoginAccount) principalCollection.getPrimaryPrincipal();
        // 判断用户名是否存在
        if (StringUtils.isEmpty(account)) {
           throw new RuntimeException("获取用户授权信息失败");
        }
        // 创建一个授权对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 进行权限设置
        List<String> permissions = account.getPermissions();
        if (permissions != null && !permissions.isEmpty()) {
            info.addStringPermissions(permissions);
        }
        // 角色设置
        List<String> roles = account.getRoles();
        if (roles != null) {
            info.addRoles(roles);
        }
        return info;
    }

    /**
     * 获取用户认证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        logger.info("##################执行Shiro登陆认证##################");
        StatelessAuthenticationToken statelessAuthenticationToken = (StatelessAuthenticationToken) authenticationToken;
        // 通过表单接收的用户名
        String token = (String)statelessAuthenticationToken.getPrincipal();
        if (StringUtils.isEmpty(token)) {
            throw new UnknownAccountException("token无效");
        }
        // 根据token获取用户信息
        LoginAccount account = userService.getAccountByToken(token);

        if (account == null) {
            throw new UnknownAccountException("token无效");
        }
        // 创建shiro的用户认证对象
        // 注意该对象的密码将会传递至后续步骤与前面登陆的subject的密码进行比对。
        //这里放入account对象后面授权可以取出来
        //statelessAuthenticationToken会与登录时候的token进行验证这里就放入登录的即可
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(account,
                statelessAuthenticationToken, getName());
        return authenticationInfo;
    }

}

这个类主要用于shiro的登录认证以及权限认证。

2.4、重写shiro的DefaultWebSubjectFactory

重写DefaultWebSubjectFactory主要是关闭创建session
 

$title(StatelessDefaultSubjectFactory.java)
package net.xqlee.project.demo.shiro.config.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

/**
 * Subject工厂重写
 * 
 * @author xq
 *
 */
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

	@Override
	public Subject createSubject(SubjectContext context) {
		// 不创建session.
		context.setSessionCreationEnabled(false);
		return super.createSubject(context);
	}

}

2.5 创建一个代理登录的filter

这个filter是无状态的重点之一
 

$title(StatelessAccessControlFilter.java)
package net.xqlee.project.demo.shiro.config.shiro;

import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * 该过滤器需在shiro配置类中加入filter链
 */
public class StatelessAccessControlFilter extends AccessControlFilter {
    private static final Logger logger = LoggerFactory.getLogger(StatelessAccessControlFilter.class);
    @Value("${token.name}")
    String tokenName;

    /**
     * 先执行:isAccessAllowed 再执行onAccessDenied
     * <p>
     * isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,
     * 如果允许访问返回true,否则false;
     * <p>
     * 如果返回true的话,就直接返回交给下一个filter进行处理。 如果返回false的话,回往下执行onAccessDenied
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
            throws Exception {
        return false;
    }

    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;
     * 如果返回false表示该拦截器实例已经处理了,将直接返回即可。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //==============================该步骤主要是通过token代理登录shiro======================
        //获取参数中的token值
        String token = request.getParameter(tokenName);//这里取的参数中的token你也可以将token放于head等
        // 生成无状态Token然后代理登录
        StatelessAuthenticationToken statelessAuthenticationToken = new StatelessAuthenticationToken(token);
        try {
            // 委托给Realm进行登录
            getSubject(request, response).login(statelessAuthenticationToken);
        } catch (UnknownAccountException ue) {
            logger.debug(ue.getLocalizedMessage());
        } catch (Exception e) {
            logger.error(e.getLocalizedMessage(), e);
            //登录失败不用处理后面的过滤器会处理并且能通过@ControllerAdvice统一处理相关异常
        }
        return true;
    }
}

 

2.6shiro的核心配置

$title(ShiroConfig.java)
package net.xqlee.project.demo.shiro.config.shiro;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import net.xqlee.project.demo.shiro.config.shiro.cache.RedisCacheManager;

/***
 *
 * @author xqlee
 *
 */
@Configuration
public class ShiroConfig {

	/**
	 * ehcache缓存方案<br/>
	 * 简单的缓存,后续可更换为redis缓存,通过自己实现shiro的CacheManager接口和Cache接口
	 * 
	 * @return
	 */
	@Bean
	public CacheManager shiroEhCacheManager() {
		EhCacheManager cacheManager = new EhCacheManager();
		cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return cacheManager;
	}

	/**
	 * redis缓存方案
	 * 
	 * @return
	 */
	@Bean
	public CacheManager shiroRedisCacheManager() {
		return new RedisCacheManager();
	}

	/****
	 * 自定义Real
	 * 
	 * @return
	 */
	@Bean
	public UserRealm userRealm() {
		UserRealm realm = new UserRealm();
		// 根据情况使用缓存器
		realm.setCacheManager(shiroRedisCacheManager());// shiroEhCacheManager()
		return realm;
	}

	/**
	 * 自定义的无状态(无session)Subject工厂
	 * 
	 * @return
	 */
	@Bean
	public StatelessDefaultSubjectFactory subjectFactory() {
		return new StatelessDefaultSubjectFactory();
	}

	/**
	 * sessionManager通过sessionValidationSchedulerEnabled禁用掉会话调度器,
	 * 因为我们禁用掉了会话,所以没必要再定期过期会话了。
	 * 
	 * @return
	 */
	@Bean
	public DefaultSessionManager sessionManager() {
		DefaultSessionManager sessionManager = new DefaultSessionManager();
		sessionManager.setSessionValidationSchedulerEnabled(false);
		//缓存
		sessionManager.setCacheManager(shiroRedisCacheManager());
		return sessionManager;
	}

	/***
	 * 安全管理配置
	 * 
	 * @return
	 */
	@Bean
	public SecurityManager defaultWebSecurityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

		// 注意这里必须配置securityManager
		SecurityUtils.setSecurityManager(securityManager);
		// 根据情况选择缓存器
		securityManager.setCacheManager(shiroRedisCacheManager());// shiroEhCacheManager()

		// 设置Subject工厂
		securityManager.setSubjectFactory(subjectFactory());

		// 配置
		securityManager.setRealm(userRealm());

		// session
		securityManager.setSessionManager(sessionManager());

		// 禁用Session作为存储策略的实现。
		DefaultSubjectDAO defaultSubjectDAO = (DefaultSubjectDAO) securityManager.getSubjectDAO();
		DefaultSessionStorageEvaluator defaultSessionStorageEvaluatord = (DefaultSessionStorageEvaluator) defaultSubjectDAO
				.getSessionStorageEvaluator();
		defaultSessionStorageEvaluatord.setSessionStorageEnabled(false);

		return securityManager;
	}

	/**
	 * 配置shiro的拦截器链工厂,默认会拦截所有请求
	 * 
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean() {
		ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
		// 配置安全管理(必须)
		filterFactoryBean.setSecurityManager(defaultWebSecurityManager());

		//配置自定义Filter
		filterFactoryBean.getFilters().put("statelessAuthc", statelessAuthcFilter());
		// 配置登陆的地址
		filterFactoryBean.setLoginUrl("/userNoLogin.do");// 未登录时候跳转URL,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		filterFactoryBean.setSuccessUrl("/welcome.do");// 成功后欢迎页面
		filterFactoryBean.setUnauthorizedUrl("/403.do");// 未认证页面


		// 配置拦截地址和拦截器
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();// 必须使用LinkedHashMap,因为拦截有先后顺序
		// statelessAuthc:所有url都必须认无状态证通过才可以访问; anon:所有url都都可以匿名访问

		filterChainDefinitionMap.put("/userNoLogin.do**", "anon");// 未登录跳转页面不设权限认证
		filterChainDefinitionMap.put("/login.do**", "anon");// 登录接口不设置权限认真
		filterChainDefinitionMap.put("/logout.do**", "anon");// 登出不需要认证

		// 下面的的其他资源地址全部需要通过代理登录步骤,注意顺序,必须先进过无状态代理登录后,后面的权限和角色认证才能使用
		filterChainDefinitionMap.put("/**", "statelessAuthc");

		filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

		// 全部配置
		// anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名访问
		//
		// authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
		// 需要登录,不需要权限和角色可访问
		//
		// authcBasic
		// org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
		//
		// perms
		// org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
		// 需要给定的权限值才能访问
		//
		// port org.apache.shiro.web.filter.authz.PortFilter
		//
		// rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
		//
		// roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
		// 需要给定的角色才能访问
		//
		// ssl org.apache.shiro.web.filter.authz.SslFilter
		//
		// user org.apache.shiro.web.filter.authc.UserFilter
		//
		// logout org.apache.shiro.web.filter.authc.LogoutFilter
		return filterFactoryBean;
	}

	/**
	 * Add.4.1 访问控制器.
	 * 
	 * @return
	 */
	@Bean
	public StatelessAccessControlFilter statelessAuthcFilter() {
		StatelessAccessControlFilter statelessAuthcFilter = new StatelessAccessControlFilter();
		return statelessAuthcFilter;
	}

	/**
	 * Add.5.1 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}

	/**
	 * Add.5.2 自动代理所有的advisor: 由Advisor决定对哪些类的方法进行AOP代理。
	 */
	@Bean
	public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
		defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
		return defaultAdvisorAutoProxyCreator;
	}
}

三、无状态访问资源创建

3.1 shiro登录

$title(LoginController.java)
package net.xqlee.project.demo.shiro.controller;

import net.xqlee.project.demo.shiro.pojo.LoginAccount;
import net.xqlee.project.demo.shiro.pojo.Result;
import net.xqlee.project.demo.shiro.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用户登录用
 *
 * @author xqlee
 */
@RestController
public class LoginController {
    private static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    UserService userService;

    /****
     * 用户未登录
     *
     * @return
     */
    @GetMapping("userNoLogin.do")
    public Object noLogin() {
        return Result.noLogin();
    }

    @GetMapping(value = "/login.do")
    public Object login(String loginName, String password) {
        try {
            LoginAccount account = userService.getLoginAccountByLoginName(loginName);
            if (account == null) {
                return Result.fail("账号不存在");
            }

            if (!account.getPassword().equals(password)) {
                return Result.fail("密码错误");
            }

            if (!account.isEnabled()) {
                return Result.fail("账号被锁");
            }
            String token = userService.createToken(account);
            return Result.success(token);
        } catch (Exception e) {
            return Result.fail(e.getMessage());
        }
    }

}

这个类主要是用于登录然后将登录成功的token返回给请求者。

3.2下面就是一个测试的资源访问了

$title(ResourcesController.java)
package net.xqlee.project.demo.shiro.controller;

import net.sf.json.JSONObject;
import net.xqlee.project.demo.shiro.pojo.Result;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@RestController
public class ResourcesController {

    @RequiresPermissions("user:h")//需要用户拥有user:h权限
    @GetMapping("/user/hello.do")
    public String hello() {
        return "Hello User, From Server";
    }

    @GetMapping("/hello.do")
	@RequiresRoles(value = "ROLE_USER")//需要用户拥有ROLE_USER角色
    public String hello2() {
        return "Hello User2,Form Server";
    }

    @RequiresPermissions("admin:h")//需要用户拥有admin:h权限
    @GetMapping("/admin/hello.do")
    public String helloAdmin() {
        return "Hello Admin, From Server";
    }

    @GetMapping("/welcome.do")//
    public String loginSuccess() {
        return "welcome";
    }

    @GetMapping("/403.do")
    public Object error403(HttpServletResponse response) {
        response.setStatus(403);
        return Result.noPermission();
    }
}

四、测试无状态shiro访问

4.1不登录直接访问
访问无需权限和无需认证的/welcome.do

welcome.do接口访问

可以看到成功访问到了无需认证和授权的接口。

接下来访问需要权限的接口:

访问需要权限的接口

 

可以看到返回了用户未登录的信息

4.2 用户登录后访问

首先是用户登录:

用户登录并返回token
用户登录并返回token

可以看到成功返回了token也就是data字段

通过token访问需要普通用户权限的接口:

访问授权接口
访问授权接口

可以看到上方已经成功访问了需要普通用户权限的接口。

接下来我们访问该用户未有权限的admin相关接口:

访问没有权限的接口
访问没有权限的接口

上方可以看到,返回了权限不足的提示。
 

好啦到这里spring boot shiro无状态化已经完成。有何疑问或者更好的建议或者需要项目源码的欢迎留言反馈。
 

源码下载:点击前往下载源码

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/243611.html

(0)
上一篇 2022年4月11日
下一篇 2022年4月11日

相关推荐

发表回复

登录后才能评论