Shiro登录和加密和验证码
https://blog.csdn.net/qq_31080089/article/details/53715910
参考源码: (设置了密码器、包含用户、角色创建、权限分配、ehcache缓存等模块的Demo)
https://github.com/huangzhenshi/spring_shiro
逻辑概括
加密逻辑
不设置加密器,只要不配置credentialsMatcher的Bean即可。也可以直接在代码中进行一次MD5加密。
设置加密器,复杂的MD5值加密,且多次加密,且加salt处理(可以参考纯洁的微笑的 SB shiro的Demo),校验密码时会调用有加密器的方法 doCredentialsMatch()
有加密器数据流程: 用户账号/密码 –> 用户账号去数据库中查询 数据库中的信息,包括salt值 –> 用户登录密码 +密码通过shiro配置文件中加密方式+加密次数+数据库的info中的salt值 –> 从而生成登录加密后的password信息,比对info的password信息,从而实现密码验证
代码
没有加密器,直接调用 getCredentials(token)
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {Object tokenCredentials = getCredentials(token);Object accountCredentials = getCredentials(info);return equals(tokenCredentials, accountCredentials);}有设置加密器,调用 hashProvidedCredentials(token, info),返回加密处理后的密码值
@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {Object tokenHashedCredentials = hashProvidedCredentials(token, info);Object accountCredentials = getCredentials(info);return equals(tokenHashedCredentials, accountCredentials);}protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {String hashAlgorithmName = assertHashAlgorithmName();return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);}
触发权限校验的逻辑
手动触发,不依赖shiro-web.jar,设置/login = anon,在login逻辑里面触发验证。设置index.jsp为 loginUrl,这样项目进入的时候跳转到 index.jsp的登录界面,输入密码,提交到/login后台进行验证
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager" /><!-- 核心配置,进入用户登录的页面,而不需要做权限认证拦截 --><property name="loginUrl" value="/index.jsp"/><property name="unauthorizedUrl" value="/403.jsp" /><property name="filterChainDefinitions"><value>/resources/** = anon/login = anon/logout = logout/** =authc</value></property></bean>配置触发,需要依赖shiro-web.jar,博客上说,第一次访问loginUrl的时候判断是不是表单提交,不是就跳转到对应的index.jsp里面,第二次表单提交的后台地址也是loginUrl,出触发权限验证,如果验证成功则跳转到 SuccessUrl界面。但是我试了很多blog的代码,不可行,实际验证成功未跳转到 successUrl
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager"/><property name="loginUrl" value="/login.action"/><property name="successUrl" value="/first.action"/>
代码实现
ShiroFilter配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager" /><!-- 核心配置,进入用户登录的页面,而不需要做权限认证拦截 --><property name="loginUrl" value="/index.jsp"/><property name="unauthorizedUrl" value="/403.jsp" /><!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 --><property name="filterChainDefinitions"><value><!-- 核心配置,开放静态资源、开放登录提交、设置注销链接Shiro会注销掉session -->/resources/** = anon/login = anon/logout = logout/** =authc</value></property></bean><!-- 安全管理器 --><bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><property name="realm" ref="customRealm" /></bean><!-- 自定义 realm --><bean id="customRealm" class="com.light.ac.web.realm.CustomRealm"><property name="credentialsMatcher" ref="credentialsMatcher"/></bean><!-- 凭证匹配器 --><bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"><property name="hashAlgorithmName" value="md5"/><property name="hashIterations" value="1"/></bean><!-- 保证实现了Shiro内部lifecycle函数的bean执行 --><bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /><!-- 开启shiro注解支持 --><bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"><property name="securityManager" ref="securityManager" /></bean>登录认证的时候会调用AuthenticatingRealm.getAuthenticationInfo()方法:
- 用登录token去从重写的realm并从数据库里面获取对应的info
- 比对token和info的password加密后的值是否相同,也就是密码是否匹配public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) {AuthenticationInfo info = getCachedAuthenticationInfo(token);// 会调用自定义realm里面的认证代码if (info == null) {info = doGetAuthenticationInfo(token);}// 会check token里面的密码和数据库info里面的是否一致if (info != null) {assertCredentialsMatch(token, info);}}
重写登录验证Realm(不带salt值认证的)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {// 获取用户名String userName = (String) token.getPrincipal();// 通过用户名获取用户对象User user = this.userService.findUserByUserName(userName);if (user == null) {return null;}// 通过 userId 获取该用户拥有的所有权限,返回值根据自己要求设置,并非固定值。Map<String,List<Permission>> permissionMap = this.permissionService.getPermissionMapByUserId(user.getId());user.setMenuList(permissionMap.get("menuList"));user.setPermissionList(permissionMap.get("permissionList"));SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());return info;}比对用户输出的密码和数据库中的密码(需要从shiro配置中获取到 加密的方式 MD5,加密的次数 N,另外从数据库的info中获取到salt值。
如果生成Info的构造函数里面有salt参数的话,则hashProvidedCredentials的实例中会加入salt值参数,如果构造函数中没有salt,则实例不会进行salt操作。
- 相关源码 HashedCredentialsMatcher.doCredentialsMatchprotected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info){CredentialsMatcher cm = getCredentialsMatcher();if (cm != null) {if (!cm.doCredentialsMatch(token, info)) {}public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {Object tokenHashedCredentials = hashProvidedCredentials(token, info);Object accountCredentials = getCredentials(info);return equals(tokenHashedCredentials, accountCredentials);}protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {Object salt = null;if (info instanceof SaltedAuthenticationInfo) {salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();} else {if (isHashSalted()) {salt = getSalt(token);}}return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());}
login方法主动触发用户验证和反馈
@RequestMapping("login")@ResponseBodypublic Result login(String userName,String password) {UsernamePasswordToken token = new UsernamePasswordToken(userName.trim(), password);Subject subject = SecurityUtils.getSubject();try {subject.login(token);}catch (UnknownAccountException e) {return Result.fail(403, "用户名不存在");}catch(IncorrectCredentialsException e) {return Result.fail(403, "密码不正确");}return Result.succeed("/manageUI");}创建用户或者修改用户密码时,生成加密密码的代码
public void test(){int hashIterations = 2;//加密的次数Object salt = "8d78869f470951332959580424d4bf4f";//盐值String pwd="123456";Object credentials = pwd.toCharArray();String hashAlgorithmName = "md5";//加密方式//密码值Object simpleHash = new SimpleHash(hashAlgorithmName, credentials,ByteSource.Util.bytes(username+salt), hashIterations);System.out.println("加密前的值"+pwd+". 加密后的密码值:----->" + simpleHash);}
salt相关
- salt的好处
- 仅仅知道加密方式(MD5)和加密次数(1次)也无法破解数据库中的密码,因为密码生成是夹杂了salt值处理
- 不同用户,相同的密码,如果salt值不同,则数据库存储的密码也不同
- salt值的选取,可以取一串无意义的数字+Username,或者一串无意义的数字+UserId,或者干脆数据库中不存储salt值,加密的时候直接取用户ID// 纯洁的微笑的demo salt值取的是: 用户名+saltSimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, //用户名userInfo.getPassword(), //密码ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+saltgetName() //realm name);public String getCredentialsSalt(){return this.username+this.salt;}
- 如果不使用salt值的配置,重写Realm里,返回info的构造函数不要包含 salt值,如下SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
添加验证码
在webapp下 创建validatecode.jsp
核心代码://设置页面不缓存response.setHeader("Pragma", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);嵌入在index.jsp,并添加刷新的方法
<input id="randomcode" name="randomcode" size="8" /><img id="randomcode_img" src="validatecode.jsp" alt="" width="56" height="20" align='absMiddle' /><a href=javascript:randomcode_refresh()>刷新</a># 刷新方法参数要 有时间戳才会触发刷新function randomcode_refresh(){var checkNumImage_=document.getElementById("randomcode_img");checkNumImage_.src="validatecode.jsp?timeStamp="+new Date().getTime();}修改shiro,不过滤validatecode.jsp
/validatecode.jsp=anon修改login的逻辑,添加验证码验证
@RequestMapping("login")@ResponseBodypublic Result login(String userName,String password,String randomcode,HttpServletRequest request) {HttpSession session=request.getSession();//取出session中的正确验证码String validateCode= (String) session.getAttribute("validateCode");if (randomcode!=null&&validateCode!=null&&!randomcode.equals(validateCode)){return Result.fail(403, "验证码输入错误");}}