💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# Shiro 安全模块 [TOC] 1. 准备 SpringMVC 开发环境; 2. 引入 Shiro 依赖; 3. 配置 ShiroConfig 类; 4. 配置自定义 Realm; 5. 登陆验证; 6. 注册验证; ## 准备 SpringMVC 开发环境 准备数据库用户表,存放用户信息。 ```sql -- ---------------------------- -- Table structure for user_t -- ---------------------------- DROP TABLE IF EXISTS `user_t`; CREATE TABLE `user_t` ( `id` varchar(32) NOT NULL, `username` varchar(64) NOT NULL, `password` varchar(64) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS=1; ``` 编写 DAO 接口,用于根据 Username 查询信息以及插入数据。 userMapper.java ```java @Repository public interface UserMapper { /** * 根据用户名查询用户信息 * @param username 用户名 * @return 将数据封装到Map类型中 */ public Map<String, Object> queryInfoByUsername(String username); /** * 插入一条数据 * @param data Map中包含id,username,password */ public void insertData(Map<String, String> data); } ``` userMapper.xml ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.myz.shirodemo.dao.UserMapper"> <select id="queryInfoByUsername" parameterType="java.lang.String" resultType="java.util.Map"> SELECT id, username, password FROM user_t WHERE username = #{username,jdbcType=VARCHAR} </select> <insert id="insertData" parameterType="java.util.Map"> INSERT INTO user_t ( id, username,password ) VALUES ( #{id, jdbcType=VARCHAR}, #{username, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR}); </insert> </mapper> ``` ## 引入 Shiro 依赖 ```xml <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.2</version> </dependency> <!-- shiro END--> ``` ## 配置 ShiroConfig 类 ```java @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 拦截器。匹配原则是最上面的最优先匹配 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); // 配置不会被拦截的链接 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/doLogin", "anon"); filterChainDefinitionMap.put("/doRegister", "anon"); filterChainDefinitionMap.put("/register", "anon"); // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/doLogout", "logout"); // 剩余请求需要身份认证 filterChainDefinitionMap.put("/**", "authc"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 未授权界面; // shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean(name = "myShiroRealm") public ShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){ ShiroRealm myShiroRealm = new ShiroRealm(); myShiroRealm.setCredentialsMatcher(matcher); return myShiroRealm; } @Bean public SecurityManager securityManager(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm(matcher)); return securityManager; } /** * 密码匹配凭证管理器 * * @return */ @Bean(name = "hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 采用MD5方式加密 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 设置加密次数 hashedCredentialsMatcher.setHashIterations(1024); return hashedCredentialsMatcher; } } ``` 1. `shirFilter`( `SecurityManagersecurityManager` ) 方法,是设置 `shiro` 的过滤规则。用于控制哪些请求需要身份认证后才能继续执行,哪些不需要认证等。 身份验证相关: - authc:基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure); - authcBasic:Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application); - logout:退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例“/logout=logout” - user:用户拦截器,用户已经身份验证 / 记住我登录的都可;示例“/**=user” - anon:匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon” 授权相关的: - roles:角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]” - perms:权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例“/user/**=perms[“user:create”]” - port:端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样 - rest:rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll); - ssl:SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样; - noSessionCreation:不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常; 2. `myShiroRealm`(`HashedCredentialsMatchermatcher`) 用于配置自定义的 `Realm` 。在 `Shiro` 中,所有有关身份认证及授权管理数据源的获取与管理,都在 `Realm` 中进行。 3. `hashedCredentialsMatcher()` 用于生成加密规则。这里采用 `MD5` 加密 `1024` 次的方式对密码进行加密处理。 4. `securityManager( `HashedCredentialsMatchermatcher` )` 将加密规则属性设置到自定义的 `ShiroRealm` 中,并将这个 `Realm` 加载到 `SecurityManager` 中。 ## 配置自定义 Realm ```java public class ShiroRealm extends AuthenticatingRealm { @Autowired private BaseService baseService; private SimpleAuthenticationInfo info = null; /** * 1.doGetAuthenticationInfo,获取认证消息,如果数据库中没有数,返回null,如果得到了正确的用户名和密码, * 返回指定类型的对象 * * 2.AuthenticationInfo 可以使用SimpleAuthenticationInfo实现类,封装正确的用户名和密码。 * * 3.token参数 就是我们需要认证的token * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 将token装换成UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken; // 获取用户名即可 String username = upToken.getUsername(); // 查询数据库,是否查询到用户名和密码的用户 Map<String, Object> userInfo = baseService.queryInfoByUsername(username); if(userInfo != null) { // 如果查询到了,封装查询结果,返回给我们的调用 Object principal = userInfo.get("username"); Object credentials = userInfo.get("password"); // 获取盐值,即用户名 ByteSource salt = ByteSource.Util.bytes(username); String realmName = this.getName(); // 将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理 info = new SimpleAuthenticationInfo(principal, credentials, salt,realmName); }else { // 如果没有查询到,抛出一个异常 throw new AuthenticationException(); } return info; } } ``` 1. 这里我只做了身份认证。新建一个 `ShiroRealm` 类继承 `AuthenticatingRealm` 类,实现 `doGetAuthenticationInfo( AuthenticationTokenauthenticationToken` ) 方法。 2. 这个方法主要就是用于获取数据库中的账户信息,以便用于和用户登录时从前台传过来的账户密码进行对比。 3. 根据用户名到用户表中查询账户名密码,并设置好盐值。这里的盐值要和 `ShiroConfig` 中的盐值规则一样。将账户名,密码,盐值, `realmName` 实例化到 `SimpleAuthenticationInfo` 中交给 `Shiro` 来管理。 4. 如果账户不存在,则抛出 `AuthenticationException` 异常。 5. 这样,每次用户进行 `login` 操作时,就会调用 `doGetAuthenticationInfo` 方法。 `Shiro` 就自动帮我们校验了账户密码是否匹配。 ## 登陆验证 ```java @Controller public class MyController { @Autowired private BaseService baseService; private final Logger logger = LoggerFactory.getLogger(MyController.class); @RequestMapping("/doLogin") public String doLogin(@RequestParam("username") String username, @RequestParam("password") String password) { // 创建Subject实例 Subject currentUser = SecurityUtils.getSubject(); // 将用户名及密码封装到UsernamePasswordToken UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { currentUser.login(token); // 判断当前用户是否登录 if (currentUser.isAuthenticated() == true) { return "/index.html"; } } catch (AuthenticationException e) { e.printStackTrace(); System.out.println("登录失败"); } return "/loginPage.html"; } @RequestMapping("/doRegister") public String doRegister(@RequestParam("username") String username, @RequestParam("password") String password) { boolean result = baseService.registerData(username,password); if(result){ return "/login"; } return "/register"; } @RequestMapping(value = "/login") public String login() { logger.info("login() 方法被调用"); return "loginPage.html"; } @RequestMapping(value = "/register") public String register() { logger.info("register() 方法被调用"); return "registerPage.html"; } @RequestMapping(value = "/hello") public String hello() { logger.info("hello() 方法被调用"); return "helloPage.html"; } } ``` 1. 在 `doLogin` 方法中,实现登录认证过程。 2. 首先获取当前 `Subject` 实例 3. 将用户名和密码封装到 `UsernamePasswordToken` 中 4. 用当前 `Subject` 实例执行 `login` 方法,传入参数为刚刚封装的 `token` 。执行 `login` 方法后, `shiro` 框架最终就会调用刚刚自定义 `ShiroRealm` 中的 `doGetAuthenticationInfo` 方法。 5. 用 `isAuthenticated()` 方法判断用户是否已经登录,如果是则跳转到登录后的页面(这里我跳转到的是 `index.html`)。如果登录失败,则走报异常,最后还是跳转到登录界面。 6. 这里我只 `catch` 了 `AuthenticationException` 异常。然而在 `AuthenticationException` 下有多个子异常,用于各种登录失败的场景,比如账户名不存在,密码不对,登录次数过多等等。大家针对不同的情况做不同的处理。但有一点建议,就是对于前台用户来说,不要暴露过多的错误信息,只是报一个登录失败即可,提高安全性。 ## 注册验证 在 `service` 中对 `DAO` 进行封装,实现信息查询以及信息注册。 ``` public interface BaseService { /** * 根据用户名查询用户信息 * @param username 用户名 * @return 将数据封装到Map类型中 */ public Map<String, Object> queryInfoByUsername(String username); /** * 注册功能 * @param username 用户名 * @param password 密码 * @return */ public boolean registerData(String username, String password); } ``` ## MD5 加密 ```java @Service public class BaseServiceImpl implements BaseService { @Autowired private UserMapper userMapper; @Override public Map<String, Object> queryInfoByUsername(String username) { return userMapper.queryInfoByUsername(username); } @Override public boolean registerData(String username, String password) { // 生成uuid String id = UUIDUtil.getOneUUID(); // 将用户名作为盐值 ByteSource salt = ByteSource.Util.bytes(username); /* * MD5加密: * 使用SimpleHash类对原始密码进行加密。 * 第一个参数代表使用MD5方式加密 * 第二个参数为原始密码 * 第三个参数为盐值,即用户名 * 第四个参数为加密次数 * 最后用toHex()方法将加密后的密码转成String * */ String newPs = new SimpleHash("MD5", password, salt, 1024).toHex(); Map<String, String> dataMap = new HashMap<>(); dataMap.put("id", id); dataMap.put("username", username); dataMap.put("password", newPs); // 看数据库中是否存在该账户 Map<String, Object> userInfo = queryInfoByUsername(username); if(userInfo == null) { userMapper.insertData(dataMap); return true; } return false; } } ``` 1. 注册时注意,由于之前配置了盐值规则及加密规则,所以这里要对用户输入的密码也做相同的处理之后再存入数据库中。 2. 使用 `SimpleHash` 类完成密码的加密。最后用 `toHex()` 将加密后的密码转成 `String` 。 ## Shiro 缓存配置 ```xml <?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" name="shirocache"> <diskStore path="java.io.tmpdir"/> <!-- 登录记录缓存 锁定10分钟 --> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro_cache" maxElementsInMemory="2000" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="0" timeToLiveSeconds="0" maxElementsOnDisk="0" overflowToDisk="true" memoryStoreEvictionPolicy="FIFO" statistics="true"> </cache> </ehcache> ```