springboot-shiro
目录
简介
shiro是apache下的一个轻量级开源项目,相对于springSecurity简单的多。
三大功能模块:
- Subject:主体,一般指用户;
- SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件;
- Realms:用于进行权限信息验证,一般需要自己实现。
细分功能:
- Authentication:身份认证/登录;
- Authorization:授权;
- Session Manager:会话管理,即登录后的session;
- Cryptography:加密,密码加密登;
- Web Support:web支持;
- Caching:缓存,用户信息、角色、权限等缓存redis等缓存中;
- Concurrency:多线程并发验证;
- Testing:测试支持;
- Run As:允许一个用户假装另一个用户(如果允许);
- Remember Me:记住密码;
开始
添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${spring.shiro.version}</version>
</dependency>
密码比较器
这个Bean定义了密码的加密方式、加密盐值和加密次数;当然也可以自己写个类继承HashedCredentialsMatcher
类,从而进一步自定义密码匹配(例如:添加密码错误次数限制);
public static String md5 = "md5";
public static int md5Time = 1;
@Bean
public CredentialsMatcher credentialsMatcher() {
// 如果要用redis,可以将RedisCacheManager作为构造参数
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密方式
credentialsMatcher.setHashAlgorithmName(md5);
//加密迭代次数
credentialsMatcher.setHashIterations(md5Time);
//true加密用的hex编码,false用的base64编码
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
认证和授权
这里其实就是配置一个认证域,
- 认证:可以获取用户输入的用户名和密码、token类型,可以设置密码匹配规则,最后需要返回一个
AuthenticationInfo
交给shiro处理(这里是可以进行很大程度上的自定义认证操作,例如:免密登录,丰富用户信息到session等); - 授权:主要是给用户设置角色和权限,需要注意的是,授权方法是在第一次访问授权的地址时才会执行;
public static String md5Salt = "1234";
@Bean
public AuthorizingRealm pwdAuthorizingRealm(CredentialsMatcher credentialsMatcher) {
return new AuthorizingRealm(credentialsMatcher) {
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 用户输入的用户名和密码
String userName = token.getPrincipal().toString();
// String userPwd = new String((char[]) token.getCredentials());
// todo: 根据用户名从数据库获取密码,这里固定为:5678
String password = "25d55ad283aa400af464c76d713c07ad";
if (userName == null) {
return null;
}
// 自定义密码加密盐值,可以不要,默认没有加密。也可以在这里自定义密码匹配。
ByteSource credentialsSalt = ByteSource.Util.bytes(md5Salt);
return new SimpleAuthenticationInfo(userName, password, credentialsSalt, getName());
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// todo: 可以根据登录名称查询角色、权限信息、同时还可以将角色信息缓存起来
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置角色
Set<String> roles = new HashSet<>();
roles.add(username);
roles.add("role_xxx");
info.setRoles(roles);
// 直接设置权限
Set<String> stringSet = new HashSet<>();
stringSet.add(username);
info.setStringPermissions(stringSet);
return info;
}
};
}
redis
开始
使用redis管理缓存和会话(可以不要);
-
参考springboot在项目中引入redis;
-
添加依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
Bean
配置好这些Bean并不代表就成功整合redis了,还要在后面的会话管理器、权限管理器中注入这些Bean;
@Bean
public RedisManager redisManager(RedisProperties redisProperties) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
redisManager.setPassword(redisProperties.getPassword());
redisManager.setTimeout(1800);
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager) {
RedisCacheManager cacheManager = new RedisCacheManager();
cacheManager.setRedisManager(redisManager);
return cacheManager;
}
会话管理器
可以设置一些会话管理器的参数,例如是否使用redis;
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 这两行用于整合redis
// sessionManager.setSessionDAO(sessionDAO);
// sessionManager.setCacheManager(cacheManager);
return sessionManager;
}
权限管理器
可以设置一些会话管理器的参数,例如是否使用redis缓存,配置一个或者多个Realm域
@Bean
public DefaultWebSecurityManager securityManager(AuthorizingRealm pwdAuthorizingRealm, DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
// 这行用于整合redis
// defaultSecurityManager.setCacheManager(cacheManager);
defaultSecurityManager.setSessionManager(sessionManager);
defaultSecurityManager.setRealm(pwdAuthorizingRealm);
// 记住密码功能,需要结合登录参数RememberMe使用
defaultSecurityManager.setRememberMeManager(new CookieRememberMeManager());
return defaultSecurityManager;
}
自定义filterMap
这其实就是个Map,可以在这个map中自定义shiro权限过滤器,其中Map的key为要自定义的权限名(如:roles、perms、authc等),Map的value为自定义的过滤方法,下面的示例代码实现了将角色过滤的判断逻辑有原来默认的and改为or(如果不需要自定义,可以不要这个Map);
public Map<String, Filter> filterMap(){
Map<String, Filter> filterMap = new HashMap<>();
// 自定义角色过滤器,将and改成or,同时role无权限返回json
filterMap.put("roles", new RolesAuthorizationFilter() {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String[] roles = (String[]) mappedValue;
if (roles == null || roles.length == 0) {
return true;
}
Subject subject = getSubject(request, response);
for (String role : roles) {
if (subject.hasRole(role)) return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
HttpServletResponse res = (HttpServletResponse) response;
res.setContentType("application/json;charset=utf-8");
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
/*res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.getWriter().write("请先登录");*/
} else {
res.setStatus(HttpStatus.FORBIDDEN.value());
res.getWriter().write("您没有访问权限");
}
return false;
}
});
// 自定义登录拦截,未登录的访问直接返回json,而不是跳转
/*filterMap.put("authc", new UserFilter(){
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/json;charset=utf-8");
res.getWriter().write("请先登录");
}
});*/
return filterMap;
}
资源权限
这个其实就是LinkedHashMap,需要注意的是这是个有顺序的Map,先插入的权限数据优先判断,另外这个Map的键是访问url规则,值为具体权限,取值有:
- authc:所有url都必须认证通过才可以访问;
- anon:所有url都都可以匿名访问
- user:如果使用rememberMe的功能可以直接访问
- perms: 该资源必须得到资源权限可以访问
- roles: 该资源必须得到角色权限才能访问
public Map<String, String> findFilterChainDefinitionMap(){
// todo: 后面这个可以直接从数据库里面获取
// 注意这个map
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//按顺序依次判断
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/login/login", "anon");
filterChainDefinitionMap.put("/login/login/pwd", "anon");
filterChainDefinitionMap.put("/login/logout", "logout");
filterChainDefinitionMap.put("/admin", "roles[admin]");
filterChainDefinitionMap.put("/user", "roles[user]");
// 注意权限设置顺序
filterChainDefinitionMap.put("/a/1", "roles[admin]");
filterChainDefinitionMap.put("/a/**", "roles[user, admin]");
// 这种默认是and,即同时拥有admin和user才能访问
filterChainDefinitionMap.put("/info", "roles[admin,user]");
filterChainDefinitionMap.put("/perms", "perms[admin]");
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
ShiroFilterFactoryBean
Shiro的核心配置类
初始化权限
@Autowired
private ShiroService shiroService;
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 上面自定义的filterMap
shiroFilterFactoryBean.setFilters(shiroService.filterMap());
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/deny");
// 上面编写资源权限Map
Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
动态更新权限
@Autowired
private ShiroFilterFactoryBean shiroFilterFactoryBean;
@Autowired
private ShiroService shiroService;
public synchronized void updatePermission() throws Exception {
AbstractShiroFilter shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
DefaultFilterChainManager manager = (DefaultFilterChainManager) ((PathMatchingFilterChainResolver)
shiroFilter.getFilterChainResolver()).getFilterChainManager();
manager.getFilterChains().clear();
shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
// 上面编写资源权限Map
Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
for (Map.Entry<String, String> entry : chains.entrySet()) {
manager.createChain(entry.getKey(), entry.getValue());
}
}
登录
@PostMapping("/login")
public String login(String username, String password, Model model, HttpServletRequest req) {
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
// 可以获取登录前的访问request对象
String url = WebUtils.getSavedRequest(req).getRequestURI();
System.out.println(url);
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (AuthenticationException e) {
// 包括未知账户、密码错误、用户名或密码错误次数过多、账户已锁定、用户名或密码不正确等异常
model.addAttribute("msg", e.getMessage());
}
if (subject.isAuthenticated()) {
Session session = subject.getSession();
// session.setTimeout(30 * 1000);
session.setAttribute("user", "这里有登录信息");
model.addAttribute("msg", "登录成功");
return "index";
} else {
token.clear();
}
return "login";
}