Springboot-shiro-redis实现登录认证和权限管理
在学习之前:
首先进行一下Apache Shiro和Shiro比较:
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦。
但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢?
-
验证用户身份
-
用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
-
在非 web 或 EJB 容器的环境下可以任意使用Session API
-
可以响应认证、访问控制,或者 Session 生命周期中发生的事件
-
可将一个或以上用户安全数据源数据组合成一个复合的用户 "view"(视图)
-
支持单点登录(SSO)功能
-
支持提供“Remember Me”服务,获取用户关联信息而无需登录。
开始代码:
pom包依赖:
<properties>
<shiro.version>1.4.0</shiro.version>
</properties>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro.version}</version>
</dependency>/**
* 自定义认证器,区分ajax请求
* @author zxs 2018年1月22日21:45:55
*/public class RoleAuthorizationFilter extends AuthorizationFilter {
-
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws IOException {
Subject subject = getSubject(request, response);
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0) {
// n
// o roles specified, so nothing to check - allow access.
return true;
}
Set<String> roles = CollectionUtils.asSet(rolesArray);
for (String role : roles) {
if (subject.hasRole(role)) {
return true;
}
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Subject subject = getSubject(request, response);
if (subject.getPrincipal() == null) {
if ("XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("X-Requested-With"))) {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Charset","UTF-8");
PrintWriter out = httpServletResponse.getWriter();
CommonResult result = new CommonResult(false);
result.setCode("401");
result.setMsg("请重新登录");
out.write(JSON.toJSONString(result));
out.flush();
out.close();
} else {
String unauthorizedUrl = getUnauthorizedUrl();
WebUtils.issueRedirect(request, response, unauthorizedUrl);
}
} else {
if ("XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("X-Requested-With"))) {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Charset","UTF-8");
PrintWriter out = httpServletResponse.getWriter();
CommonResult result = new CommonResult(false);
result.setCode("403");
result.setMsg("没有足够的权限: "+((HttpServletRequest) request).getServletPath());
out.println(JSON.toJSONString(result));
out.flush();
out.close();
} else {
String unauthorizedUrl = getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(403);
}
}
}
return false;
}
}ShiroConf:shiro配置类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。/** * shiro配置
* @author zxs 2018年1月22日21:10:37
*/
@Configuration
public class ShiroConf {
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new DelegatingFilterProxy("shiroFilter"));
filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
//密码验证方式 数据库保存的密码是使用sha算法加密的,所以这里需要配置一个密码匹配对象
@Bean
public RetryLimitHashedCredentialsMatcher credentialsMatcher() {
RetryLimitHashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("sha");
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);//是否存储散列后的密码为16进制,需要和生成密码时的一样
credentialsMatcher.setRetryCount(5);
credentialsMatcher.setRetryTime(1800000);
return credentialsMatcher;
}
//根据用户名和密码校验登陆
@Bean
public UsernameRealm usernameRealm(RetryLimitHashedCredentialsMatcher credentialsMatcher) {
UsernameRealm usernameRealm = new UsernameRealm();
usernameRealm.setCredentialsMatcher(credentialsMatcher);
usernameRealm.setCachingEnabled(true);
return usernameRealm;
}
//配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. -->
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
// 调用我们配置的权限管理器
@Bean
public DefaultWebSecurityManager securityManager(UsernameRealm usernameRealm) {
DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
dwsm.setRealm(usernameRealm);
return dwsm;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(defaultWebSecurityManager);
return aasa;
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, ApplicationContext context) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/sys/auth");//这里是设置登录路径
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/auth/logout");//您请求的资源不再您的权限范围,则跳转到这里
Map<String, Filter> filters = new LinkedHashMap();
// filters.put("logout", logoutFilter);
filters.put("roles", new RoleAuthorizationFilter());
shiroFilterFactoryBean.getFilters().putAll(filters);//加载自定义拦截器
SysResService resService = context.getBean(SysResService.class);//只有通过这种方式才能获得resService,因为此处会优先于resService实例化
loadShiroFilterChain(shiroFilterFactoryBean,resService);//加载拦截规则
return shiroFilterFactoryBean;
}
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean,SysResService resService) {
Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
//默认拦截规则
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/sys/auth/login", "anon");
filterChainDefinitionMap.put("/assets/**", "anon");
filterChainDefinitionMap.put("/data/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/winline/**", "anon");
filterChainDefinitionMap.put("/sys/auth/**", "anon");
filterChainDefinitionMap.put("/file/**", "anon");
filterChainDefinitionMap.put("/error/403", "anon");
filterChainDefinitionMap.put("/error/404", "anon");
filterChainDefinitionMap.put("/error/500", "anon");
//用户自定义拦截规则
filterChainDefinitionMap = resService.loadFilterChainDefinitions(filterChainDefinitionMap);
//过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//都不满足的时候,需要超级管理员权限才能访问
filterChainDefinitionMap.put("/**", "roles[ROLE_SUPER]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
/*@Bean
public ShiroDialect shiroDialect() {//thymeleaf 集成shiro使用,如果没有可以删除
return new ShiroDialect();
}*/
}定义用户过滤器:
/**
* 自定义用户过滤器
* @author zy 2018年1月22日21:10:40
*
*
*/
public class SysUserFilter extends PathMatchingFilter {
@Autowired
private SysUserService sysUserService;
@Override
protected boolean onPreHandle(ServletRequest req, ServletResponse rep, Object mappedValue)
throws Exception {
String username = (String) SecurityUtils.getSubject().getPrincipal();
HttpServletRequest request = (HttpServletRequest) req;
HttpSession session = request.getSession();
//在session域中加入当前登陆的用户信息
SysUser sysUser = (SysUser) session.getAttribute(SystemConstant.SYS_CURRENT_USER);
if(sysUser == null){
session.setAttribute(SystemConstant.SYS_CURRENT_USER, sysUserService.getByUsername(username));
}
return super.onPreHandle(req, rep, mappedValue);
}
}根据用户名和密码校验登陆:
我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。
/**
* 根据用户名和密码校验登陆
*
* @author zxs 2018年1月22日21:18:39
*/
public class UsernameRealm extends AuthorizingRealm {
/* 实现Realm类MyShiro继承自AuthorizingRealm,AuthorizingRealm实现它的抽象方法doGetAuthorizationInfo权限角色进行配置,AuthorizingRealm又继承自AuthenticatingRealm,AuthenticatingRealm也有一个抽象方法doGetAuthenticationInfo,实现doGetAuthenticationInfo方法对登录的令牌等信息进行验证。*/
/**
* 系统用户service
*/
@Autowired
@Lazy
private SysUserService sysUserService;
/**
* 加载用户授权信息, 包括权限资源和角色\用户组资源
*
* @author zxs 2018年1月22日21:31:39
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 登陆名
String username = (String) principals.getPrimaryPrincipal();
if (username != null) {
//权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = sysUserService.loadEnabledRolesByUsername(username);
if (roles.contains(SystemConstant.ROLE_SUPER)) {
authorizationInfo.addStringPermission("*");
} else {
// 加载权限资源
authorizationInfo.setStringPermissions(sysUserService.loadEnabledPermissionsByUsername(username));
}
// 加载角色/用户组
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
return null;
}
/**通过SimpleAuthenticationInfo将盐值以及用户名和密码信息封装到AuthenticationInfo中,进入证书凭证类中进行校验
* 加载用户身份认证信息
*
* @author zxs 2018年1月22日21:31:39
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 获取账号信息
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null) {
throw new UnknownAccountException(); // 没找到帐号
}
if (sysUser.getStatus() == DataStatus.LOGIC_DELETE.getValue()) {
throw new UnknownAccountException(); // 没找到帐号
}
if (sysUser.getStatus() == DataStatus.DISABLE.getValue()) {
throw new LockedAccountException(); // 帐号锁定
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(sysUser.getUsername(),
sysUser.getPwd(), ByteSource.Util.bytes(sysUser.getSalt()),
getName());
// 此处无需比对,比对的逻辑Shiro会获取到数据库的用户名和密码
//我们只需返回一个和令牌相关的正确的验证信息,
return authenticationInfo;
}
}
登录控制器:登录过程其实只是处理异常的相关信息,具体的登录验证交给shiro来处理.
@RequestMapping("login")
public ModelAndView login(String username, String password, boolean rememberMe, HttpSession session, Model model, HttpServletRequest request) {
ModelAndView mv = new ModelAndView("redirect:/my/index");
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
//提交申请 调用到Subjectsubject = securityManager.login(this, token);方法后,则跳转到自定义Realm中
subject.login(token);
SysUser user = userService.getByUsername(username);
user.setLastLoginTime(new Date());
user = userService.save(user);
//在session中保存当前用户的个人信息
session.setAttribute(SystemConstant.SYS_CURRENT_USER, user);
//记录登录信息
operLogService.login(username, IpUtils.getRemoteHost(request));
//获取登陆前访问的页面
SavedRequest savedRequest = WebUtils.getSavedRequest(request);
System.out.println("获取登陆前访问的页面"+savedRequest);
if (savedRequest != null) {
String requestUrl = savedRequest.getRequestUrl();
if (StringUtils.isNoneBlank(requestUrl)) {
mv.setViewName("redirect:"+requestUrl);
}
}
} catch (Exception e) {
mv.setViewName("forward:/sys/auth");
mv.addObject("errMsg","用户名或密码错误");
mv.addObject("username",username);
mv.addObject("password","password");
mv.addObject("rememberMe","rememberMe");
Logger.error(AuthController.class,e.getMessage(), e.getStackTrace());
}
return mv;
}
密码验证方式:实现了在五分钟内 用户五次输入密码的机会。
/**
* 密码验证方式
* @author zxs 2018年1月22日21:28:55
*/
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private RedisTemplate<Serializable,AtomicInteger> redisTemplate;
private final String PREFIX_USER_RETRY_COUNT = "_RETYR_";
/**在单位时间内连续尝试登录的限制次数*/
private Integer retryCount = 5;
/**redisTemplateg过期时间300s*/
private Integer retryTime = 300 ;
private Cache<String, AtomicInteger> passwordRetryCache;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String)token.getPrincipal();
//retry count + 1 线程安全,多个线程共享变量
AtomicInteger retryCount = redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username);
//如果登录成功,那么这个count就会从缓存中移除,从而实现了如果登录次数超出指定的值就锁定。
if(retryCount != null) {
if(retryCount.incrementAndGet() > 5) {//连续尝试+1登录次数异常(incrementAndGet返回新值,而getAndIncrement返回旧值)
throw new ExcessiveAttemptsException();
}
redisTemplate.opsForValue().set(PREFIX_USER_RETRY_COUNT+username,retryCount,retryTime, TimeUnit.SECONDS);
System.out.println("redisTemplate.opsForValue():"+redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username));
}else{
retryCount = new AtomicInteger(1);
redisTemplate.opsForValue().set(PREFIX_USER_RETRY_COUNT+username,retryCount,300, TimeUnit.SECONDS);
}
boolean matches = super.doCredentialsMatch(token, info);
System.out.println("matches:"+matches);
if(matches) {
//clear retry count 验证成功即删除
redisTemplate.delete(PREFIX_USER_RETRY_COUNT+username);
System.out.println("redisTemplate.opsForValue()清空了:"+redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username));
}
return matches;
}
public Integer getRetryCount() {
return retryCount;
}
public void setRetryCount(Integer retryCount) {
this.retryCount = retryCount;
}
public Integer getRetryTime() {
return retryTime;
}
public void setRetryTime(Integer retryTime) {
this.retryTime = retryTime;
}
}
账号加密:
/**
* 账号加密
* @author zxs 2018年1月22日21:29:26
*/
public class Encryp {
/** 随机字符生产工具 */
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
/** 加密方式 */
@Value("${shiro.password.algorithmName}")
private String algorithmName = "sha";
/** 多重加密次数 */
@Value("${shiro.password.hashIterations}")
private int hashIterations = 2;
/**
* 配置随机字符生产工具
* @param randomNumberGenerator
*
* @author zxs 2018年1月22日21:30:00
*/
public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) {
this.randomNumberGenerator = randomNumberGenerator;
}
/**
* 配置加密方式
* @param algorithmName
*
* @author zxs 2018年1月22日21:30:00
*/
public void setAlgorithmName(String algorithmName) {
this.algorithmName = algorithmName;
}
/**
* 配置重复加密次数
* @param hashIterations
*
* @author zxs 2018年1月22日21:30:00
*/
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
/**
* 密码加密
* @param user 用户信息
*
* @author zxs 2018年1月22日21:30:00
*/
public void encryptPassword(SysUser user) {
user.setSalt(randomNumberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(algorithmName, user.getPwd(), ByteSource.Util.bytes(user.getSalt()), hashIterations).toHex();
user.setPwd(newPassword);
}
/**
* 根据私钥加密
* @param value 要加密字段
* @param salt 密钥
* @author zxs 2018年1月22日21:30:00
* @return 加密后字段
*/
public String encrypt(String value , String salt) {
return new SimpleHash(algorithmName, value, ByteSource.Util.bytes(salt), hashIterations).toHex();
}
/**
* 获取加密后的新密码
*
* @param pwd 密码
* @param salt 盐
* @return 新密码
* @author zxs 2018年1月22日21:30:00
*/
public String getEncryptPassword(String pwd,String salt){
String newPassword = new SimpleHash(algorithmName, pwd, ByteSource.Util.bytes(salt), hashIterations).toHex();
return newPassword;
}
}