shiro是apache下的一个轻量级开源项目,相对于springSecurity简单的多。
三大功能模块:
细分功能:
添加依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>${spring.shiro.version}version>
dependency>
这个Bean定义了密码的加密方式、加密盐值和加密次数;当然也可以自己写个类继承HashedCredentialsMatcher
类,从而进一步自定义密码匹配(例如:添加密码错误次数限制);
@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;
}
这里其实就是配置一个认证域,
AuthenticationInfo
交给shiro处理(这里是可以进行很大程度上的自定义认证操作,例如:免密登录,丰富用户信息到session等);@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管理缓存和会话(可以不要);
参考springboot在项目中引入redis;
添加依赖
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>3.2.3version>
dependency>
配置好这些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);
return defaultSecurityManager;
}
这其实就是个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规则,值为具体权限,取值有:
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;
}
Shiro的核心配置类
@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;
}
@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";
}