未完待续。。。
一、Shiro介绍
Shiro 是 Apache 旗下的一个开源安全认证框架,是安全认证方面的一个 Java 类库,实现用户身份认证,权限授权,加密,会话管理(Session)等功能。下面,是 Shiro 中的一些核心概念。
1.1 Realm
领域,处理用户的认证、授权,需要继承 AuthorizingRealm 类,自行重写认证、授权方法。(从数据库获取用户权限信息、密码加密校验等)
1.2 SecurityManager
安全管理器,Shiro 的核心,对用户的认证、授权进行安全管理。
1.3 SessionManager
会话管理器,Shiro 框架自己实现的不依赖Web容器的会话管理,并且可以将Session缓存搭配Redis中,所以非web项目也可以使用Shiro。
1.4 CacheManager
缓存管理。可以将用户的权限信息缓存到内存或者Redis中。
二、整合Shiro
2.1 引入pom依赖
<!-- Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.1</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.1</version>
</dependency>
2.2 授权与认证
package com.xxx.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @Author: yiwenli
* @Description: ShiroRealm shiro与应用安全数据间的"桥梁"或"连接器"
* @Date: 2022/10/21
*/
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {
@Value("${shiro.username:admin}")
private String shiroUsername;
@Value("${shiro.password:admin}")
private String shiroPassword;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("执行了授权doGetAuthorizationInfo");
return null;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
log.info("执行了认证doGetAuthenticationInfo");
// 强转shiro封装成的token
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// TODO 正常应该去数据库中查用户信息
// 用户名认证
if (!userToken.getUsername().equals(shiroUsername)) {
return null;
}
// 密码认证
return new SimpleAuthenticationInfo(shiroUsername, shiroPassword, this.getName());
}
}
2.3 自定义密码校验
package com.xxx.shiro;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* @Author: yiwenli
* @Description: 自定义密码校验,默认会调用shiro默认的
* @Date: 2022/10/26
*/
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken tokenResolve = (UsernamePasswordToken) token;
String tokenPwd = new String(tokenResolve.getPassword());
String infoPwd = (String) info.getCredentials();
// 调用当前类重写的equals方法来对比两个password是否一致,返回对比结果
return super.equals(tokenPwd, infoPwd);
}
}
2.4 自定义过滤器配置(解决中文路径400问题)
package com.xxx.shiro;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import javax.servlet.Filter;
import java.util.Map;
/**
* @Author: yiwenli
* @Description: 用于解决shiro导致中文路径400非法问题
* @Date: 2022/10/25
*/
public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {
@Override
protected FilterChainManager createFilterChainManager() {
FilterChainManager manager = super.createFilterChainManager();
Map<String, Filter> filterMap = manager.getFilters();
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
if (invalidRequestFilter instanceof InvalidRequestFilter) {
// 此处是关键,设置false跳过URL携带中文400,servletPath中文校验
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
}
return manager;
}
}
2.5 自定义认证拦截器(根据需求确定重定向到页面还是直接返回数据)
package com.xxx.shiro;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
/**
* @Author: yiwenli
* @Description: 自定义Authc拦截器:用于解决未登录成功时也会将session信息存到redis中的问题,不再重定向到登录页
* @Date: 2022/10/31
*/
public class AuthcShiroFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception{
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
// option请求处理
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
resp.setStatus(HttpStatus.OK.value());
return true;
}
// this.saveRequestAndRedirectToLogin(request, response);
// 取消重定向,直接返回结果
returnTokenInvalid((HttpServletRequest)request, (HttpServletResponse)response);
return false;
}
}
/**
* 替代shiro重定向
*/
private void returnTokenInvalid(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setContentType("application/json; charset=utf-8");
resp.setCharacterEncoding("UTF-8");
Writer out = new BufferedWriter(new OutputStreamWriter(resp.getOutputStream()));
out.write("无法访问");
out.flush();
out.close();
}
}
2.6 配置类
package com.xxx.shiro;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: yiwenli
* @Description: Shiro配置类
* @Date: 2022/10/21
*/
@Configuration
public class ShiroConfig {
/**
* 引入定义好的域
*/
@Bean(name = "shiroRealm")
public ShiroRealm shiroRealm(){
// realm(领域)连接数据
return new ShiroRealm();
}
/**
* 密码校验
*/
@Bean(name = "myCredentialsMatcher")
public MyCredentialsMatcher myCredentialsMatcher(){
return new MyCredentialsMatcher();
}
/**
* session配置
*/
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
// 会话过期时间
defaultWebSessionManager.setGlobalSessionTimeout(1000 * 60 * 30);
defaultWebSessionManager.setSessionValidationInterval(1000 * 60 * 30);
defaultWebSessionManager.setDeleteInvalidSessions(true);
defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
defaultWebSessionManager.setSessionIdCookieEnabled(true);
// tomcat的JESSIONID自动生成模块
defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
return defaultWebSessionManager;
}
/**
* 安全管理器配置
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager webSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm,
@Qualifier("myCredentialsMatcher") MyCredentialsMatcher myCredentialsMatcher,
@Qualifier("sessionManager") DefaultWebSessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 密码校验放入域中
shiroRealm.setCredentialsMatcher(myCredentialsMatcher);
// 将域添加到安全管理器中
securityManager.setRealm(shiroRealm);
// 设置session管理器
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
MyShiroFilterFactoryBean factoryBean = new MyShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(defaultWebSecurityManager);
// 设置未登录用户跳转页面
factoryBean.setLoginUrl("/t/html/t-login.html");
// 设置登录成功返回页面
// factoryBean.setSuccessUrl("/t/html/t-tingdian-index.html");
// 设置未授权返回页面
// factoryBean.setUnauthorizedUrl();
// 过滤定义
Map<String, Filter> filtersMap = new LinkedHashMap<>();
/*// 配置自定义登出 覆盖logout之前默认的LogoutFilter
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/t/html/t-login.html");
filtersMap.put("logout", logoutFilter);*/
/*// 配置自定义认证拦截器
filtersMap.put("authc", new AuthcShiroFilter());*/
factoryBean.setFilters(filtersMap);
// 过滤链定义,从上向下顺序执行,必须是链式,保证有序
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 设置无需认证就可以访问的页面
filterChainDefinitionMap.put("/t/html/t-login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/getPublicKeyStr", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.json", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
// 最简单版登录页引用的静态文件
// filterChainDefinitionMap.put("/t/pic/login-back.png", "anon");
// filterChainDefinitionMap.put("/t/pic/browserLogo.ico", "anon");
// 开启shiro内置退出过滤器,完成退出功能(不用自己写退出方法了)
// filterChainDefinitionMap.put("/logout", "logout");
// 设置必须认证才能访问的页面
filterChainDefinitionMap.put("/**", "authc");
// 设置shiro的内置过滤器,配置访问权限
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
}
2.7 登录相关功能控制器
package com.xxx.tingdian.controller;
import com.ieslab.common.RSAUtil;
import com.ieslab.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: yiwenli
* @Description: 登录Controller
* @Date: 2022/10/26
*/
@Slf4j
@RestController
public class LoginController {
/**
* 获取数据加密公钥
*/
@GetMapping(value = "/getPublicKeyStr", produces = "application/json;charset=utf-8")
public String getPublicKeyStr() {
return Result.success(RSAUtil.getPublicKeyStr()).toJsonString();
}
/**
* 用户登录
*/
@PostMapping(value = "/login", produces = "application/json;charset=utf-8")
public String login(String username, String password) {
if (StringUtils.isBlank(username)) {
return Result.error("用户名不能为空").toJsonString();
}
if (StringUtils.isBlank(password)) {
return Result.error("密码不能为空").toJsonString();
}
Subject subject = SecurityUtils.getSubject();
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, RSAUtil.decrypt(password));
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
e.printStackTrace();
return Result.error("用户名错误").toJsonString();
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
return Result.error("密码错误").toJsonString();
}
return Result.success().toJsonString();
}
/**
* 用户退出登录
*/
@GetMapping(value = "/logout", produces = "application/json;charset=utf-8")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return Result.success().toJsonString();
}
}