springboot系列(十)springboot整合shiro实现登录认证
扫码入群,可获取一手资料和直播课程。
关于shiro的概念和知识本篇不做详细介绍,但是shiro的概念还是需要做做功课的要不无法理解它的运作原理就无法理解使用shiro;
本篇主要讲解如何使用shiro实现登录认证,下篇讲解使用shiro实现权限控制
要实现shiro和springboot的整合需要以下几大步骤:
- 生成用户表
- 引入shiro依赖
- 添加shiro配置文件
- 添加自定义的realm
- 登录操作触发验证
- 细节处理
下面我们一步步的详细介绍:
一、生成用户表
CREATE TABLE `sys_user` ( `user_id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) DEFAULT NULL COMMENT '密码', `salt` varchar(20) DEFAULT NULL COMMENT '盐', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `mobile` varchar(100) DEFAULT NULL COMMENT '手机号', `status` tinyint(4) DEFAULT NULL COMMENT '状态 0:禁用 1:正常', `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`user_id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='系统用户';
二、引入shiro依赖
<!-- Apache shiro依赖 只需要引入本依赖 shiro-spring 会自动引入shiro-web和shiro-core依赖--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
三、添加shiro的配置文件(本篇使用的是@Configuration注解java类的方式,也可以使用xml的方式)
package com.chuhouqi.demo.shiro; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Bean("sessionManager") public SessionManager sessionManager(){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setSessionIdUrlRewritingEnabled(false); //sessionManager.setSessionIdCookieEnabled(false); return sessionManager; } @Bean("securityManager") public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); securityManager.setSessionManager(sessionManager); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); shiroFilter.setLoginUrl("/login"); shiroFilter.setSuccessUrl("/index"); shiroFilter.setUnauthorizedUrl("/403"); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/druid/**", "anon"); filterMap.put("/api/**", "anon"); filterMap.put("/login", "anon"); filterMap.put("/registe", "anon"); filterMap.put("/registe.html", "anon"); filterMap.put("/**/*.css", "anon"); filterMap.put("/**/*.js", "anon"); // filterMap.put("/login.html", "anon"); filterMap.put("/fonts/**", "anon"); filterMap.put("/plugins/**", "anon"); filterMap.put("/swagger/**", "anon"); filterMap.put("/favicon.ico", "anon"); filterMap.put("/captcha.jpg", "anon"); filterMap.put("/", "anon"); filterMap.put("/**", "authc"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator(); proxyCreator.setProxyTargetClass(true); return proxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
四、添加自定义的realm(实现认证和授权)
package com.chuhouqi.demo.shiro; import com.chuhouqi.demo.common.utils.ShiroUtil; import com.chuhouqi.demo.entity.User; import com.chuhouqi.demo.service.IUserService; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserRealm extends AuthorizingRealm { @Autowired private IUserService userService; @Override /** * 权限授权 */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override /** * 登录认证 */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户输入的用户名 String username = (String) token.getPrincipal(); //根据用户名查询用户信息 User user = userService.getUser(username); // 账号不存在 if (user == null) { throw new UnknownAccountException("账号不存在"); } // 账号锁定 if (user.getStatus() == 0) { throw new LockedAccountException("账号已被锁定,请联系管理员"); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); return info; } /** * 设置密码比较器为HashedCredentialsMatcher * @param credentialsMatcher */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher(); shaCredentialsMatcher.setHashAlgorithmName(ShiroUtil.hashAlgorithmName); shaCredentialsMatcher.setHashIterations(ShiroUtil.hashIterations); super.setCredentialsMatcher(shaCredentialsMatcher); } }
五、登录操作触发验证
package com.chuhouqi.demo.controller; import com.chuhouqi.demo.common.utils.ShiroUtil; import com.chuhouqi.demo.entity.User; import com.chuhouqi.demo.service.IUserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginController { private static Logger logger = LoggerFactory.getLogger(LoginController.class); @Autowired private IUserService userService; @GetMapping("/login") String login() { return "login"; } @RequestMapping("/login") public String login(String username, String password, Model model){ try { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); subject.login(token); }catch (UnknownAccountException e) { logger.error(e.getMessage()); model.addAttribute("msg",e.getMessage()); return "login"; }catch (IncorrectCredentialsException e) { logger.error(e.getMessage()); model.addAttribute("msg","账号或密码不正确"); return "login"; }catch (LockedAccountException e) { logger.error(e.getMessage()); model.addAttribute("msg","账号已被锁定,请联系管理员"); return "login"; }catch (AuthenticationException e) { logger.error(e.getMessage()); model.addAttribute("msg","账户验证失败"); return "login"; } return "index"; } @RequestMapping("/registe") public String registe(User user){ userService.saveUser(user); return "ok"; } @RequestMapping("/logout") public String logout(){ ShiroUtil.logout(); return "redirect:/login"; } }
验证:
启动项目,然后随便请求一个路径都会被shiro配置的filter拦截器进行拦截,如果请求的路径需要权限认证就会进入shiro的认证管理中,如果当前用户没有登录就会调整到登录页面;
六、细节处理:
上面的介绍只是给出了一个大概的流程,其中有很多细节还是要特比注意的要不会导致认证失败,下面我们看一下有哪些细节需要处理
1、用户密码加密处理
在数据库中存储的用户密码不应该是123456这样的密码明文,被不法分子看到是很危险的,所以数据库中的密码应该是对密码进行加密后的密文,而且还要求这个加密算法是不可逆的,即由加密后的字符串不能反推回来原来的密码,如果能反推回来那这个加密是没有意义的。
现在常用的加密算法有: MD5,SHA1
而且shiro提供了SimpleHash这个加密工具来实现密码加密:
public final static String hashAlgorithmName = "SHA-256";//加密算法
public final static int hashIterations = 16;//hash加密次数
public static String encrypt(String pwd,String salt){
String newPassword = new SimpleHash(hashAlgorithmName,pwd,salt,hashIterations).toHex();
return newPassword;
}
如果两个人的密码一样,即存在数据表里中的两个加密后的字符串一样,然而我们希望即使两个人的密码一样,加密后的两个字符串也不一样。即需要用到MD5盐值加密。
盐值需要唯一: 一般使用随机字符串或 user id
这里提供一个工具:
String salt = RandomStringUtils.randomAlphanumeric(20);//使用随机数函数生成salt
2、配置shiro的密码比较器
上面我们用加密算法实现了密码的明文加密,现在数据库中存储的是密码密文,用户登录时使用的密码原文,如果不告诉shiro我们的密码加密算法逻辑,shiro是使用默认的比较器
进行的简单的密码比较(即使用数据库中的密码密文和用户登录时输入的密码明文进行比较),显而易见这样比较是不会成功的所以我们要告诉shiro 我们是使用的加密算法,
实现过程很简单:在我们自定义的UserRealm中添加如下配置
/** * 设置密码比较器为HashedCredentialsMatcher * @param credentialsMatcher */ @Override public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher(); shaCredentialsMatcher.setHashAlgorithmName(ShiroUtil.hashAlgorithmName);//这里就是我们进行密码加密的算法 shaCredentialsMatcher.setHashIterations(ShiroUtil.hashIterations);//加密循环次数 super.setCredentialsMatcher(shaCredentialsMatcher); }