shiro搭建总体流程总结
整体的搭建流程:#
总体的代码结构如图:
-
自定义Realm
重写授权和认证方法,doGetAuthorizationInfo和doGetAuthenticationInfo方法,并且设置密码加密匹配算法,设置开启缓存。
/** * @author :RealGang * @description:自定义权限匹配和密码匹配,认证用户,授权 * @date : 2021/10/18 18:04 */ public class MyShiroRealm extends AuthorizingRealm { private final static Logger logger = LoggerFactory.getLogger(MyShiroRealm.class); /** * 延迟加载bean,解决缓存Cache不能正常使用;事务Transaction注解不能正常运行 */ @Autowired @Lazy private UserServiceImpl userService; public MyShiroRealm() { //设置凭证匹配器,修改为hash凭证匹配器 HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher(); //设置算法 myCredentialsMatcher.setHashAlgorithmName("md5"); //散列次数 myCredentialsMatcher.setHashIterations(1024); this.setCredentialsMatcher(myCredentialsMatcher); //开启缓存 this.setCachingEnabled(true); this.setAuthenticationCachingEnabled(true); this.setAuthorizationCachingEnabled(true); } /** * @description: 授权;doGetAuthorizationInfo方法是在我们调用;SecurityUtils.getSubject().isPermitted()这个方法,授权后用户角色及权限会保存在缓存中的 * "@RequiresPermissions"这个注解其实就是在执行SecurityUtils.getSubject().isPermitted() * 授权 * 这个方法在每次访问ShiroConfig里面配置的受保护资源时都会调用 * 因此,需要做缓存 * @param: principalCollection * @return: org.apache.shiro.authz.AuthorizationInfo * @author: RealGang * @date: 2021/10/19 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user; Object object = principalCollection.getPrimaryPrincipal(); // 这里用json转化为USER,因为可能从redis获取的用户信息反序列化不能强制转换为user报错 if (object instanceof User) { user = (User) object; } else { user = JSON.parseObject(JSON.toJSON(object).toString(), User.class); } String username = user.getUsername(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next(); //查询数据库 user = userService.findUserInfo(user.getUsername()); logger.info("##################执行Shiro权限授权##################user info is:{}" + JSONObject.toJSONString(user)); Set<String> userPermissions = new HashSet<String>(); Set<String> userRoles = new HashSet<String>(); for (Role role : user.getRoles()) { userRoles.add(role.getRoleName()); List<Permission> rolePermissions = role.getPermissions(); for (Permission permission : rolePermissions) { userPermissions.add(permission.getPermName()); } } //角色名集合 info.setRoles(userRoles); //权限名集合,将权限放入shiro中, // 这里可以把url,按钮,菜单,api等当做资源来进行权限控制,从而对用户进行权限控制 info.addStringPermissions(userPermissions); return info; } /** * @description: 认证,登录,doGetAuthenticationInfo这个方法是在用户登录的时候调用的;也就是执行SecurityUtils.getSubject().login()的时候调用;(即:登录验证),验证通过后会用户保存在缓存中的 * @param: authenticationToken * @return: org.apache.shiro.authc.AuthenticationInfo * @author: RealGang * @date: 2021/10/19 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("##################执行Shiro登录认证##################"); // 客户端传来的 username 和 password 会自动封装到 token,先根据 username 进行查询.如果返回 null,则表示用户名错误,直接 return null 即可,Shiro 会自动抛出 UnknownAccountException 异常。 if(authenticationToken==null){ return null; } String principal = (String) authenticationToken.getPrincipal(); //查询数据库 User user = userService.findByUserName(principal); //放入shiro.调用CredentialsMatcher检验密码 if (user != null) { // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验 // 第三个参数一般也可以是ByteSource.Util.bytes(shiroUser.getUserName()+shiroPasswordService.getPublicSalt()) // //由于shiro-redis插件需要从这个属性中获取id作为redis的key,所有这里传的是user而不是username // return new SimpleAuthenticationInfo(user, user.getPassWord(), credentialsSalt, this.getClass().getName()); return new SimpleAuthenticationInfo(user, user.getPassword(), new CurrentSalt(user.getSalt()), this.getClass().getName()); } return null; } }
-
添加Shiro自定义会话#
添加自定义会话ID生成器#
这里配置token以"login_token"开头的token也就是sessionId
public class ShiroSessionIdGenerator implements SessionIdGenerator { /** *实现SessionId生成 * @param session * @return */ @Override public Serializable generateId(Session session) { Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session); return String.format("login_token_%s", sessionId); } }
添加自定义会话管理器#
public class ShiroSessionManager extends DefaultWebSessionManager { //定义常量 private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; //重写构造器 public ShiroSessionManager() { super(); this.setDeleteInvalidSessions(true); } /** * 重写方法实现从请求头获取Token便于接口统一 * * 每次请求进来, * Shiro会去从请求头找Authorization这个key对应的Value(Token) * @param request * @param response * @return */ @Override public Serializable getSessionId(ServletRequest request, ServletResponse response) { String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION); //如果请求头中存在token 则从请求头中获取token if (!StringUtils.isEmpty(token)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return token; } else { // 这里禁用掉Cookie获取方式 return null; // 否则按默认规则从cookie取sessionId //return super.getSessionId(request, response); } } }
-
配置shiro:shiroConfig
在该配置文件里主要是一个filterFactoryBean,该类里注册SecurityManager,并且可以设置一些自定义过滤器,然后设置过滤url规则,在securityManager方法里注入自定义的realm,并且注入自己重写的redisCacheManager和会话管理器sessionManager
@Configuration public class ShiroConfig { // CACHE_KEY里是缓存AuthenticationInfo信息和AuthorizationInfo信息的缓存名称的前缀 private static final String CACHE_KEY = "shiro:cache:"; private static final String SESSION_KEY = "shiro:session:"; private static final int EXPIRE = 18000; @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; // @Value("${spring.redis.password}") // private String password; @Value("${spring.redis.jedis.pool.min-idle}") private int minIdle; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.max-active}") private int maxActive; //开启对shior注解的支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * @description: 自定义过滤器 MyShiroRealm,我们的业务逻辑全部定义在这个 bean 中。 * @param: * @return: com.example.autohomingtest.config.MyShiroRealm * @author: RealGang * @date: 2021/10/19 */ @Bean public MyShiroRealm myShiroRealm(){ return new MyShiroRealm(); } /** * @description: 将 myShiroRealm 注入到 DefaultWebSecurityManager bean 中,完成注册。 * @param: myShiroRealm * @return: org.apache.shiro.web.mgt.DefaultWebSecurityManager * @author: RealGang * @date: 2021/10/19 */ @Bean public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm){ DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myShiroRealm); manager.setCacheManager(redisCacheManager()); // 这里是我自己增加的,不然sessionManager没有注册进去 manager.setSessionManager(sessionManager()); SecurityUtils.setSecurityManager(manager); return manager; } /** * @description: ShiroFilterFactoryBean,这是 Shiro 自带的一个 Filter 工厂实例,所有的认证和授权判断都是由这个 bean 生成的 Filter 对象来完成的, * 这就是 Shiro 框架的运行机制,开发者只需要定义规则,进行配置,具体的执行者全部由 Shiro 自己创建的 Filter 来完成。 * @param: manager * @return: org.apache.shiro.spring.web.ShiroFilterFactoryBean * @author: RealGang * @date: 2021/10/19 */ @Bean public ShiroFilterFactoryBean filterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){ ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); Map<String, Filter> filterMap = factoryBean.getFilters(); // 这里把自定义拦截OPTIONS请求的拦截器注册进来 filterMap.put("authc",new MyShiroFilter()); Map<String,String> map = new LinkedHashMap<>(); /** * Shiro 内置过滤器,过滤链定义,从上向下顺序执行 * 常用的过滤器: * anon:无需认证(登录)可以访问 * authc:必须认证才可以访问 * user:只要登录过,并且记住了密码,如果设置了rememberMe的功能可以直接访问 * perms:该资源必须得到资源权限才可以访问 * role:该资源必须得到角色的权限才可以访问 */ map.put("/manage","perms[manage]"); map.put("/administrator","roles[administrator]"); //anon表示可以匿名访问 map.put("/index", "anon"); map.put("/login", "anon"); map.put("/static/**", "anon"); map.put("/user/testDb","anon"); //authc表示需要登录 map.put("/user/**","authc"); map.put("/main","authc"); factoryBean.setFilterChainDefinitionMap(map); //设置登录页面,覆盖默认的登录url,这里如果未认证会跳转到/unauthc这里来 factoryBean.setLoginUrl("/unauthc"); //未授权页面 factoryBean.setUnauthorizedUrl("/unauthr"); // 登录成功后要跳转的链接 factoryBean.setSuccessUrl("/index"); return factoryBean; } /** * 配置Redis管理器 * @Attention 使用的是shiro-redis开源插件 * @return */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setTimeout(timeout); // redisManager.setPassword(password); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(maxIdle+maxActive); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMinIdle(minIdle); redisManager.setJedisPoolConfig(jedisPoolConfig); return redisManager; } @Bean public RedisCacheManager redisCacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setKeyPrefix(CACHE_KEY); // shiro-redis要求放在session里面的实体类必须有个id标识 //这是组成redis中所存储数据的key的一部分,完整的key的形式:shiro:cache:com.example.autohomingtest.config.MyShiroRealm.authenticationCache:username redisCacheManager.setPrincipalIdFieldName("username"); return redisCacheManager; } /** * SessionID生成器 * */ @Bean public ShiroSessionIdGenerator sessionIdGenerator(){ return new ShiroSessionIdGenerator(); } /** * 配置RedisSessionDAO */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setSessionIdGenerator(sessionIdGenerator()); redisSessionDAO.setKeyPrefix(SESSION_KEY); redisSessionDAO.setExpire(EXPIRE); return redisSessionDAO; } /** * 配置Session管理器 * @Author Sans * */ @Bean public SessionManager sessionManager() { ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); shiroSessionManager.setSessionDAO(redisSessionDAO()); //禁用cookie shiroSessionManager.setSessionIdCookieEnabled(false); //禁用会话id重写 // ession管理器的setSessionIdUrlRewritingEnabled(false)配置没有生效,导致没有认证直接访问受保护资源出现多次重定向的错误。将shiro版本切换为1.5.0后就解决了这个bug。 shiroSessionManager.setSessionIdUrlRewritingEnabled(false); return shiroSessionManager; } }
-
解决跨域问题
配置CorConfig:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600) .allowedHeaders("*"); } }
配置MyShiroFilter过滤器,这里主要拦截OPTIONS请求,并且注册到上述的filterFactoryBean中去:
public class MyShiroFilter extends FormAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (request instanceof HttpServletRequest) { if (((HttpServletRequest)request).getMethod().toUpperCase().equals("OPTIONS")) { return true; } } // if (request.getAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID)!=null) { // return true; // } return super.isAccessAllowed(request, response, mappedValue); } }
-
自定义盐值生成方法保证可以序列化(由于shiro当中的ByteSource没有实现序列化接口,缓存时会发生错误,因此,我们需要通过自定义ByteSource的方式实现这个接口):
public class CurrentSalt implements ByteSource, Serializable { private static final long serialVersionUID = 125096758372084309L; private byte[] bytes; private String cachedHex; private String cachedBase64; public CurrentSalt(){ } public CurrentSalt(byte[] bytes) { this.bytes = bytes; } public CurrentSalt(char[] chars) { this.bytes = CodecSupport.toBytes(chars); } public CurrentSalt(String string) { this.bytes = CodecSupport.toBytes(string); } public CurrentSalt(ByteSource source) { this.bytes = source.getBytes(); } public CurrentSalt(File file) { this.bytes = (new CurrentSalt.BytesHelper()).getBytes(file); } public CurrentSalt(InputStream stream) { this.bytes = (new CurrentSalt.BytesHelper()).getBytes(stream); } public static boolean isCompatible(Object o) { return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream; } public void setBytes(byte[] bytes) { this.bytes = bytes; } @Override public byte[] getBytes() { return this.bytes; } @Override public String toHex() { if(this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); } return this.cachedHex; } @Override public String toBase64() { if(this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); } return this.cachedBase64; } @Override public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } @Override public String toString() { return this.toBase64(); } @Override public int hashCode() { return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0; } @Override public boolean equals(Object o) { if(o == this) { return true; } else if(o instanceof ByteSource) { ByteSource bs = (ByteSource)o; return Arrays.equals(this.getBytes(), bs.getBytes()); } else { return false; } } private static final class BytesHelper extends CodecSupport { private BytesHelper() { } public byte[] getBytes(File file) { return this.toBytes(file); } public byte[] getBytes(InputStream stream) { return this.toBytes(stream); } } }
-
登录接口编写:
@RequestMapping("/login")
@ResponseBody
public ResponseWrapper loginUser(@RequestBody User user) throws AuthenticationException {
ResponseWrapper responseWrapper;
boolean flags = authcService.login(user);
if (flags){
// 将会话信息存储在redis中,并在用户认证通过后将会话Id以token的形式返回给用户。用户请求受保护资源时带上这个token,我们根据token信息去redis中获取用户的权限信息,从而做访问控制。
Serializable id = SecurityUtils.getSubject().getSession().getId();
logger.debug("会话ID:"+id);
responseWrapper=ResponseWrapper.markSuccess();
responseWrapper.setExtra("token",id);
}else {
responseWrapper = ResponseWrapper.markNoData();
}
return responseWrapper;
}
在authService里的login方法里,通过用户名和密码生成UsernamePasswordToken然后调用subject.login(token)让shiro自己去处理:
@Service
public class AuthcServiceImpl implements AuthcService {
@Override
public boolean login(User user) throws AuthenticationException {
if (user==null){
return false;
}
if (user.getUsername()==null||"".equals(user.getUsername())){
return false;
}
if (user.getPassword() == null || "".equals(user.getPassword())){
return false;
}
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
subject.login(token);
return true;
}
}
前端获取到token之后可以放到vuex里去,每次向后台发送请求可以在拦截器里将该token添加到Header里的“Authorization"里去
前端跨域问题:#
在vue.config.js文件里把上边的before: require('./mock/mock-server.js'),注释掉,并添加下边的代码
更改.dev.development文件里的VUE_APP_BASE_API
把utils文件夹里的request.js文件里的下边的code!=20000改为code!=200(这个看不同前端项目而定,如果这里不改,即使获取到了后台的代码,后台默认是200为正确的,这里前台判定是20000,前台就会报错,而不是返回后台的数据显示)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~