本文主要是使用spring boot 集成shiro 实现 shiro-spring-boot-starter
1、首先创建相关配置文件有两个 ShiroProperties,JwtProperties
package com.shiro.sdk.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "shiro")
public class ShiroProperties {
/**
* 登录路径
*/
private String loginUrl;
/**
* 登出路径
*/
private String logoutUrl;
/**
* 首页路径
*/
private String indexUrl;
/**
* 未授权跳转路径
*/
private String unauthorizedUrl;
/**
* 要忽略的url
*/
private String ignoresUrlList;
/**
* 是否启用ShiroJwt true 启用 false 禁用 , 默认启用
*/
private boolean enable = true;
/**
* 是否支持跨域访问 默认为 true
*/
private boolean corsEnable = true;
/**
* 是否启用 token 自动刷新
*/
private boolean tokenRefreshEnable = true;
/**
* token放在 http HEADER 里面的 key名称,默认为 Authorization
*/
private String tokenHeaderKey = "Authorization";
/**
* 是否开启缓存 默认缓存组件MemoryConstrainedCacheManager, 默认不开启
*/
private boolean enableCacheMemory = false;
public String getLoginUrl() {
return loginUrl;
}
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
public String getLogoutUrl() {
return logoutUrl;
}
public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
}
public String getIgnoresUrlList() {
return ignoresUrlList;
}
public void setIgnoresUrlList(String ignoresUrlList) {
this.ignoresUrlList = ignoresUrlList;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public boolean isCorsEnable() {
return corsEnable;
}
public void setCorsEnable(boolean corsEnable) {
this.corsEnable = corsEnable;
}
public boolean isTokenRefreshEnable() {
return tokenRefreshEnable;
}
public void setTokenRefreshEnable(boolean tokenRefreshEnable) {
this.tokenRefreshEnable = tokenRefreshEnable;
}
public String getTokenHeaderKey() {
return tokenHeaderKey;
}
public void setTokenHeaderKey(String tokenHeaderKey) {
this.tokenHeaderKey = tokenHeaderKey;
}
public boolean isEnableCacheMemory() {
return enableCacheMemory;
}
public void setEnableCacheMemory(boolean enableCacheMemory) {
this.enableCacheMemory = enableCacheMemory;
}
public String getIndexUrl() {
return indexUrl;
}
public void setIndexUrl(String indexUrl) {
this.indexUrl = indexUrl;
}
public String getUnauthorizedUrl() {
return unauthorizedUrl;
}
public void setUnauthorizedUrl(String unauthorizedUrl) {
this.unauthorizedUrl = unauthorizedUrl;
}
}
package com.shiro.sdk.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "shiro.jwt")
public class JwtProperties {
/**
* jwt 的签发者 默认为 trj
*/
private String jwtIssuer = "trj";
/**
* jwt 的主题 默认为 jwtAuthToken
*/
private String jwtSubject = "jwtAuthToken";
/**
* jwt token 过期秒数
*/
private Integer tokenExpireSeconds;
/**
* jwt token 刷新秒数,表示 是否已经发布了 tokenRefreshSeconds 秒,如果超过就刷新一个新的token给前端
*/
private Integer tokenRefreshSeconds;
/**
* 签名秘钥
*/
private String secretKey;
public String getJwtIssuer() {
return jwtIssuer;
}
public void setJwtIssuer(String jwtIssuer) {
this.jwtIssuer = jwtIssuer;
}
public String getJwtSubject() {
return jwtSubject;
}
public void setJwtSubject(String jwtSubject) {
this.jwtSubject = jwtSubject;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public Integer getTokenExpireSeconds() {
return tokenExpireSeconds;
}
public void setTokenExpireSeconds(Integer tokenExpireSeconds) {
this.tokenExpireSeconds = tokenExpireSeconds;
}
public Integer getTokenRefreshSeconds() {
return tokenRefreshSeconds;
}
public void setTokenRefreshSeconds(Integer tokenRefreshSeconds) {
this.tokenRefreshSeconds = tokenRefreshSeconds;
}
}
2、创建shiro配置类 ShiroJwtAutoConfig
package com.shiro.sdk.config;
import com.shiro.sdk.filter.JwtAuthorizationFilter;
import com.shiro.sdk.properties.JwtProperties;
import com.shiro.sdk.properties.ShiroProperties;
import com.shiro.sdk.realm.JwtAuthorizingRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Configuration
@EnableConfigurationProperties({JwtProperties.class, ShiroProperties.class})
@ConditionalOnProperty(prefix = "shiro", value = "enable", havingValue = "true", matchIfMissing = true)
public class ShiroJwtAutoConfig implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Resource
private ShiroProperties shiroProperties;
@Resource
private JwtProperties jwtProperties;
/*private JwtAuthorizingRealm jwtAuthorizingRealm;
@Bean
@ConditionalOnMissingBean
public JwtAuthorizingRealm jwtAuthorizingRealm() {
log.debug("Shiro Bean JwtAuthorizingRealm 初始化 ...");
Map<String, JwtAuthorizingRealm> beans = applicationContext.getBeansOfType(JwtAuthorizingRealm.class);
if (beans == null || beans.size() == 0) {
throw new RuntimeException("请继承 JwtAuthorizingRealm 并实现它的方法");
}
for(JwtAuthorizingRealm jwtAuthorizingRealm : beans.values()){
jwtAuthorizingRealm = jwtAuthorizingRealm;
break;
}
jwtAuthorizingRealm.setCredentialsMatcher(new JwtCredentialsMatcher(jwtProperties));
if (shiroProperties.isEnableCacheMemory()) {
// 使用默认提供的 MemoryConstrainedCacheManager
jwtAuthorizingRealm.setCacheManager(new MemoryConstrainedCacheManager());
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
jwtAuthorizingRealm.setAuthenticationCachingEnabled(true);
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false,一旦配置了缓存管理器,授权缓存默认开启
jwtAuthorizingRealm.setAuthorizationCachingEnabled(true);
}
return jwtAuthorizingRealm;
}*/
@Bean
@ConditionalOnMissingBean
public DefaultWebSecurityManager securityManager() {
log.info("Shiro Bean SecurityManager 初始化 ...");
// 交由DefaultWebSecurityManager管理
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
Map<String, JwtAuthorizingRealm> beans = applicationContext.getBeansOfType(JwtAuthorizingRealm.class);
manager.setRealms(new ArrayList<>(beans.values()));
/*
* 使用JWT方式进行访问控制,因此关闭 shiro 自带的 session管理,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
// 禁用remberMe功能
manager.setRememberMeManager(null);
return manager;
}
@Bean
@ConditionalOnMissingBean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
log.info("Shiro Bean ShiroFilterFactoryBean 初始化 ...");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl());
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl(shiroProperties.getIndexUrl());
// 未授权跳转页面
shiroFilterFactoryBean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());
//配置需要忽略的路径
LinkedHashMap<String, String> ignoresUrlMap = getIgnoresUrlMap(shiroProperties.getIgnoresUrlList());
shiroFilterFactoryBean.setFilterChainDefinitionMap(ignoresUrlMap);
//配置相关过滤器
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>(3);
filterMap.put("jwtAuthorizationFilter", new JwtAuthorizationFilter(shiroProperties));
/*filterMap.put("jwtPermissionFilter", new JwtPermissionFilter());
filterMap.put("jwtRoleFilter", new JwtRoleFilter());*/
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterRuleMap = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwtAuthorizationFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
private LinkedHashMap<String, String> getIgnoresUrlMap(String ignoresUrlList) {
if(StringUtils.isBlank(ignoresUrlList)){
return new LinkedHashMap();
}
String[] urls = StringUtils.split(ignoresUrlList, ",");
if(urls == null || urls.length <= 0){
return new LinkedHashMap();
}
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(urls.length);
Arrays.stream(urls).map(url -> { map.put(url, "anon");return map; }).collect(Collectors.toList());
return map;
}
/**
* 开启shiro注解支持,例如 @RequiresPermissions
* @param securityManager DefaultWebSecurityManager
* @return AuthorizationAttributeSourceAdvisor
*/
@Bean
@ConditionalOnMissingBean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
3、JwtAuthenticationToken implements AuthenticationToken
Object getPrincipal() 与Object getCredentials() 在这里不实现,交由业务方去实现
package com.shiro.sdk.token;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtAuthenticationToken implements AuthenticationToken {
private String JwtAuthenticationToken;
public JwtAuthenticationToken(String JwtAuthenticationToken){
this.JwtAuthenticationToken = JwtAuthenticationToken;
}
@Override
public Object getPrincipal() {
return JwtAuthenticationToken;
}
@Override
public Object getCredentials() {
return JwtAuthenticationToken;
}
}
4、JwtAuthorizingRealm extends AuthorizingRealm
package com.shiro.sdk.realm;
import com.shiro.sdk.token.JwtAuthenticationToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.AuthorizingRealm;
public abstract class JwtAuthorizingRealm extends AuthorizingRealm{
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtAuthenticationToken;
}
}
5、在项目resources下建 META-INF 文件夹并建 spring.factories 文件,里面加入如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.shiro.sdk.config.ShiroJwtAutoConfig
6、打包生成 相关jar
7、业务方使用时的步骤:
因为shiro是一个集合了认证授权的框架,因此既可以用来做登陆认证,也可以用来做用户授权,本次已经将shiro结合spring boot 做成了一个可配置的sdk,方便以后其他项目的使用。
使用时只需配置4步即可完成授权认证的相关功能
1、pom.xml 文件引入相关sdk
<dependency>
<groupId>com.shiro.authorization</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.0.1-SNAPSHOT</version>
</dependency>
2、用户登陆时调用shiro的用户认证api,用例如下
@RequestMapping(value = "login", method = RequestMethod.POST)
public Response<String> login(String username, String password){
String token = new JwtUtil(jwtProperties).sign(username, password);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(jwtAuthenticationToken);
return new Response<String>().success(token);
} catch (Exception e) {
e.printStackTrace();
log.error("登录失败,用户名[{}]", username, e);
return new Response<String>().fail("登录失败", e.getMessage());
}
}
具体与shiro有交互的代码有四行
//使用jwtUtil生成一个token(这里可以是jwt也可以是其他的)
String token = new JwtUtil(jwtProperties).sign(username, password);
//将生成的token封装为一个JwtAuthenticationToken参数供shiro在认证时使用
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
//调用 SecurityUtils 获取一个subject(可以理解为与shiro交互的api接口类,与我们的service类似)
Subject subject = SecurityUtils.getSubject();
//调用 subject.login;进行认证
subject.login(jwtAuthenticationToken)
3、具体的用户认证实现与相关权限实现
/**
* Shiro-权限相关的业务处理
*/
@Slf4j
@Service
public class ShiroServiceImpl extends JwtAuthorizingRealm implements ShiroService {
@Resource
private JwtProperties jwtProperties;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (log.isDebugEnabled()) {
log.debug("进入授权 doGetAuthorizationInfo");
log.debug("the toke is {}", principals.toString());
log.debug("realmName = {}", getName());
}
// 该用户具有哪些权限,这里需要从数据库查数据,为了测试方便造了条数据
String username = new JwtUtil(jwtProperties).getUsername(principals.toString());
//HashSet<String> permissionsSet = permissionService.get(username)
HashSet<String> permissionsSet = new HashSet();
permissionsSet.add("edit");
authorizationInfo.addStringPermissions(permissionsSet);
// 该用户具有哪些角色,这里需要从数据库查数据,为了测试方便造了条数据
//HashSet<String> roleSet = roleService.get(username)
HashSet<String> roleSet = new HashSet();
roleSet.add("admin");
authorizationInfo.addRoles(roleSet);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = new JwtUtil(jwtProperties).getUsername(token);
if (username == null) {
throw new AuthenticationException("token invalid");
}
//这里需要从数据库查出用户,为了测试方便所以造了条数据
//User userBean = userService.getUser(username);
User userBean = new User();
userBean.setUserName("zhangsan");
userBean.setPassword("123456");
if (userBean == null) {
throw new AuthenticationException("User didn't existed!");
}
boolean checkTokenResult = false;
try {
checkTokenResult = new JwtUtil(jwtProperties).verify(token, username, userBean.getPassword());
}catch (TokenExpiredException e){
throw new UnauthenticatedException("token 已失效");
}catch (Exception e){
throw e;
}
if (! checkTokenResult) {
throw new AuthenticationException("Username or password error");
}
return new SimpleAuthenticationInfo(token, token, "jwtAuthorizingRealm");
}
}
首先需要定义一个bean 实现 JwtAuthorizingRealm 这个类中的 doGetAuthorizationInfo,doGetAuthenticationInfo 这两个方法
doGetAuthorizationInfo 这个方法是用来加载当前用户所拥有的权限(通常是需要根据当前用户从数据库中查出来)
doGetAuthenticationInfo 这个方法是用来做具体的用户登录认证
最后需要将 定义好的bean加入spring容器,这里使用的是 spring @Service 注解,其他只要能加载到spring中的方式也都可以
4、业务方法中如果有相关接口需要做权限控制,直接使用shiro相关权限控制注解,用例如下
@Slf4j
@RestController
@RequestMapping("/")
public class IndexController {
@Resource
private JwtProperties jwtProperties;
@RequestMapping(value = "login", method = RequestMethod.POST)
public Response<String> login(String username, String password){
String token = new JwtUtil(jwtProperties).sign(username, password);
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(jwtAuthenticationToken);
return new Response<String>().success(token);
} catch (Exception e) {
e.printStackTrace();
log.error("登录失败,用户名[{}]", username, e);
return new Response<String>().fail("登录失败", e.getMessage());
}
}
/**
* 无需做权限控制的接口
* */
@RequestMapping(value = "anon", method = RequestMethod.POST)
public Response<String> anon(){
return new Response<String>().success(null);
}
/**
* 需要登录才能访问的接口
* */
@RequiresAuthentication
@RequestMapping(value = "/require", method = RequestMethod.POST)
public Response<String> require(){
return new Response<String>().success(null);
}
/**
* 需要相关角色才能访问的接口
* */
@RequiresRoles(value = {"user"})
@RequestMapping(value = "/role", method = RequestMethod.POST)
public Response<String> role(){
return new Response<String>().success(null);
}
/**
* 需要相关权限才能访问的接口
* */
@RequiresPermissions(value = {"edit"})
@RequestMapping(value = "/permission", method = RequestMethod.POST)
public Response<String> permission(){
return new Response<String>().success(null);
}
}
@RequiresAuthentication 此注解表示此接口只有登录用户才能访问
@RequiresRoles(value = {"user"}) 此注解表示此接口只有拥有user角色的用户才能访问 value是一个数组说明可以设置多个角色,他们之间默认是且的关系
@RequiresPermissions(value = {"edit"}) 此注解表示只有拥有edit权限的用户才能访问 value是一个数组说明可以设置多个权限,他们之间默认是且的关系
shiro-spring-boot-starter 已提交到码云
shiro-spring-boot-test 已提交到码云
有问题可加微信
补充一句,加微信别老是您您您的,都是打工人,不必这么客气,我也才18啊哈哈
请大家关注下博客谢谢