Shiro安全框架
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
主要功能
shiro主要有三大功能模块:
Subject:主体,一般指用户。
SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
Realms:用于进行权限信息的验证,一般需要自己实现。
细分功能
- Authentication:身份认证/登录(账号密码验证)。
- Authorization:授权,即角色或者权限验证。
- Session Manager:会话管理,用户登录后的session相关管理。
- Cryptography:加密,密码加密等。
- Web Support:Web支持,集成Web环境。
- Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
- Remember Me:记住我,登录后,下次再来的话不用登录了。
Maven依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
快速入门语句
// 得到DefaultSecurityManager对象
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
// 读取ini配置文件
IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
// 配置DefaultSecurityManager对象
defaultSecurityManager.setRealm(iniRealm);
// 获取SecurityUtils对象
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 获取当前用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
// 通过当前用户获取Session
Session session = currentUser.getSession();
//判断用户是否被认证
currentUser.isAuthenticated()
//通过Token进行登录操作
currentUser.login(token)
//根据输入账户名和密码获取Token
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//判断用户的身份
currentUser.hasRole("schwartz")
//判断用户拥有的权限
currentUser.isPermitted("lightsaber:wield")
//注销当前用户
currentUser.logout();
SpringBoot继承Shiro
Maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
创建Realm类
public class CustomRealm extends AuthorizingRealm {
@Autowired
private LoginService loginService;
/**
* @MethodName doGetAuthorizationInfo
* @Description 权限配置类
* @Param [principalCollection]
* @Return AuthorizationInfo
* @Author WangShiLin
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
//将用户拥有的权限加载到获取权限中
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* @MethodName doGetAuthenticationInfo
* @Description 认证配置类
* @Param [authenticationToken]
* @Return AuthenticationInfo
* @Author WangShiLin
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(),ByteSource.Util.bytes("x23*2d"),getName());
return simpleAuthenticationInfo;
}
}
创建Realm类继承AuthorizingRealm,重写doGetAuthorizationInfo(授权配置)、doGetAuthenticationInfo(认证配置)方法。
其中AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。
其中ByteSource.Util.bytes方法为用户设置时的随机盐值。
创建ShiroConfig配置类
@Configuration
public class ShiroConfig {
//将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm myShiroRealm = new CustomRealm();
//设置realm hash验证
HashedCredentialsMatcher credentialsMatcher= new HashedCredentialsMatcher();
//使用加密方法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数
credentialsMatcher.setHashIterations(1024);
myShiroRealm.setCredentialsMatcher(credentialsMatcher);
return myShiroRealm;
}
//权限管理,配置主要是Realm的管理认证
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager());
//绑定Reaml
securityManager.setRealm(myShiroRealm);
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
// 必须使用session管理器,才能够解决前后端分离shiro的subject未认证的问题
@Bean
public SessionManager sessionManager(){
//将我们继承后重写的shiro session 注册
ShiroSession shiroSession = new ShiroSession();
//如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSession;
}
/**
* Shiro生命周期处理器
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
其中shiro内置过滤器:
anno:无需认证即可访问
authc:必须认证才可以访问
user :不许拥有记住我功能才能访问
perms:拥有对某个资源访问权限才能使用 ((perms认证必须放在authc认证前,否则无效))
role:拥有某个角色权限才能访问
权限限定访问:
map.put("/set","perms[user:set]"); //只限定拥有‘user:set’权限的用户访问
ShiroSessionManager类
/**
* 目的: shiro 的 session 管理
* 自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
* shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,
* 调用 storeSessionId 方法保存sesionId 到 cookie中
* 为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
* 自定义生成sessionId 则要实现 SessionIdGenerator
*
*/
public class ShiroSession extends DefaultWebSessionManager {
/**
* 定义的请求头中使用的标记key,用来传递 token
*/
private static final String AUTH_TOKEN = "authToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSession() {
super();
//设置 shiro session 失效时间,默认为30分钟,这里现在设置为35分钟
setGlobalSessionTimeout(MILLIS_PER_MINUTE * 35);
}
/**
* 获取sessionId,原本是根据sessionKey来获取一个sessionId
* 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
* 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
* @param request ServletRequest
* @param response ServletResponse
* @return Serializable
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
if (StringUtils.isEmpty(sessionId)){
//如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
return super.getSessionId(request, response);
} else {
//请求头中如果有 authToken, 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
//sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
shiro认证是通过SessionId来进行判断是否认证.
在用户登录成功时获取SecurityUtils的Session ID(不是值)用作autoToken:
Subject Usersubject = SecurityUtils.getSubject();
// shiro的sessionID
String authToken = (String) Usersubject.getSession().getId();
这个Session管理类是用作跨域访问时要求 前台request请求头部传递一个 token (内容为SessionId) 来认证其是否 认证通过.
Controller类
@RestController
@Slf4j
public class LoginController {
@GetMapping("/login")
public String login(User user) {
if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
return "请输入用户名和密码!";
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}
return "login success";
}
...
}
1.用 SecurityUtils.getSubject()获取Subject类。
2.将用户输入进去的账户密码信息封装入UsernamePasswordToken类。
3.使用Subject类的login方法判断登录结果,并捕捉相关错误异常。
登录错误异常
UnknownAccountException: 用户名不存在
AuthenticationException: 账户或者密码错误
AuthorizationException: 没有权限
Account Exception : 账号异常
- ConcurrentAccessException: 并发访问异常(多个用户同时登录时抛出)
- UnknownAccountException:未知的账号
- ExcessiveAttemptsException: 认证次数超过限制
- DisabledAccountException: 禁用的账号
- LockedAccountException: 账号被锁定
- UnsupportedTokenException: 使用了不支持的Token
Shiro跨域过滤
/**
* @description: Shiro跨域请求过滤
* @author: Zhaotianyi
* @time: 2021/5/18 15:56
*/
@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "shiroLoginFilter")
public class ShiroLoginFilter implements Filter {
private FilterConfig config = null;
@Override
public void init(FilterConfig config) throws ServletException {
this.config = config;
}
@Override
public void destroy() {
this.config = null;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 允许哪些Origin发起跨域请求,nginx下正常
// response.setHeader( "Access-Control-Allow-Origin", config.getInitParameter( "AccessControlAllowOrigin" ) );
response.setHeader( "Access-Control-Allow-Origin", "*" );
// 允许请求的方法
response.setHeader( "Access-Control-Allow-Methods", "HEAD,POST,GET,OPTIONS,DELETE,PUT" );
// 多少秒内,不需要再发送预检验请求,可以缓存该结果
response.setHeader( "Access-Control-Max-Age", "3600" );
// 表明它允许跨域请求包含xxx头
response.setHeader( "Access-Control-Allow-Headers", "*" );
//是否允许浏览器携带用户身份信息(cookie)
response.setHeader( "Access-Control-Allow-Credentials", "true" );
// response.setHeader( "Access-Control-Expose-Headers", "*" );
if (request.getMethod().equals( "OPTIONS" )) {
response.setStatus( 200 );
return;
}
filterChain.doFilter( servletRequest, response );
}
}
Shiro+Thymeleaf页面整合
Maven依赖:
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
Themeleaf页面头部加入 xmlns:shiro=”http://www.pollix.at/thymeleaf/shiro" 开启代码提示。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
...
</html>
常用标签:
The has Permission tag
shiro:hasPermission=”xxx” 判断当前用户是否拥有xxx权限
<div shiro:hasPermission="user:set"></div>
The authenticated tag
authenticated=“” 已经用户得到认证
<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a>
The hasRole tag
shiro:hasRole=”xxx” 判断当前用户为xxx权限
<a shiro:hasRole="administrator" href="admin.html">Administer the system</a>
权限、角色访问控制
方法一:直接在页面控制(以Thymeleaf为例)
<!--拥有user:add权限的任何人才能看见-->
<div shiro:hasPermission="user:add:*">
<a th:href="@{/user/add}">Add</a>
</div>
<!--拥有admin角色才能看见-->
<div shiro:hasRole="admin">
<a th:href="@{/user/update}">Update</a>
</div>
方法二:Controller代码层中控制
//获取当前用户
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {
System.out.println("添加成功!");
}else{
System.out.println("添加失败!");
方法三:代码注释控制
@RequestMapping("/user/add")
@RequiresRoles("admin") //判断角色
@RequiresPermissions("user:add:*") //判断权限
public String add() {
return "user/add";
}
Ehcache缓存持久化
Shiro支持很多第三方缓存工具。官方提供了shiro-ehcache,实现了把EHCache当做Shiro的缓存工具的解决方案。其中最好用的一个功能是就是缓存认证执行的Realm方法,减少对数据库的访问。
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.2</version>
</dependency>
shiro-ehcache是Shiro官方与Ehcache进行对接的依赖包。
我们只需要在其ShiroConfig配置类中进行增加其Ehcache功能即可:
...
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
...
manager.setCacheManager(ehCacheManager());
return manager;
}
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
InputStream is = null;
try {
is = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
e.printStackTrace();
}
net.sf.ehcache.CacheManager cacheManager = new net.sf.ehcache.CacheManager(is);
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
...