本文共 25451 字,大约阅读时间需要 84 分钟。
为了在网欲音乐系统中实现登入功能,在此使用到了shiro这个安全框架,从而使用到了多Realm认证——用户名密码、手机短信验证。特整理这篇文章希望能让后来者少踩一点坑。
本篇文章环境:SpringBoot 2.0 + shiro+ redis
使用阿里云手机号发送短信验证码
1、用户密码登录realm
package com.music.system.shiro.realm;import com.music.model.User;import com.music.service.impl.UserService;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;/** * 用户密码登录realm */@Slf4jpublic class UserPasswordRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override public String getName() { return LoginType.USER_PASSWORD.getType(); } @Override public boolean supports(AuthenticationToken token) { if (token instanceof UserToken) { return ((UserToken) token).getLoginType() == LoginType.USER_PASSWORD; } else { return false; } } @Override public void setAuthorizationCacheName(String authorizationCacheName) { super.setAuthorizationCacheName(authorizationCacheName); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 认证信息.(身份验证) : Authentication 是用来验证用户身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { log.info("---------------- 用户密码登录 ----------------------"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); // 从数据库获取对应用户名密码的用户 User user = userService.findUserByName(name); if (user != null) { // 用户为禁用状态 if (!user.getShow()) { throw new DisabledAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 user.getPassword(), //密码 getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException(); } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; }}
2、手机验证码登录realm
package com.music.system.shiro.realm;import com.music.model.User;import com.music.service.impl.UserService;import com.music.utils.JedisClient;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import javax.annotation.Resource;/** * 手机验证码登录realm */@Slf4jpublic class UserPhoneRealm extends AuthorizingRealm { @Autowired private UserService userService; @Resource JedisClient jedisClient; @Override public String getName() { return LoginType.USER_PHONE.getType(); } @Override public boolean supports(AuthenticationToken token) { if (token instanceof UserToken) { return ((UserToken) token).getLoginType() == LoginType.USER_PHONE; } else { return false; } } @Override public void setAuthorizationCacheName(String authorizationCacheName) { super.setAuthorizationCacheName(authorizationCacheName); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } /** * 认证信息.(身份验证) : Authentication 是用来验证用户身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { log.info("---------------- 手机验证码登录 ----------------------"); UserToken token = (UserToken) authcToken; String phone = token.getUsername(); // 手机验证码 String validCode = String.valueOf(token.getPassword()); System.out.println("validCode------>"+validCode); // 这里从redis中获取了验证码为 123456,并对比密码是否正确 String redisCode = jedisClient.get(phone+"code"); System.out.println("redisCode------>"+redisCode); //线上用redisCode取代123456 if(!"123456".equals(validCode)){ log.debug("验证码错误,手机号为:{}", phone); throw new IncorrectCredentialsException(); } User user = userService.findUserByPhone(phone); if(user == null){ throw new UnknownAccountException(); } // 用户为禁用状态 if(!user.getShow()){ throw new DisabledAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 validCode, //密码 getName() //realm name ); return authenticationInfo; } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; }}
ShiroConfig
package com.music.system.config;import com.music.system.shiro.CredentialsMatcher;import com.music.system.shiro.MyModularRealmAuthenticator;import com.music.system.shiro.SessionManager;import com.music.system.shiro.filter.AuthcShiroFilter;import com.music.system.shiro.filter.SessionControlFilter;import com.music.system.shiro.realm.AuthorizationRealm;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserPasswordRealm;import com.music.system.shiro.realm.UserPhoneRealm;import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.realm.Realm;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.servlet.SimpleCookie;import org.crazycake.shiro.RedisCacheManager;import org.crazycake.shiro.RedisManager;import org.crazycake.shiro.RedisSessionDAO;import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;@Configurationpublic class ShiroConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; @Value("${spring.redis.password}") private String redisPassword; @Value("${spring.redis.database}") private int database; @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 由于已经重写了authc的拦截器,此处设置的loginUrl和unauthorizedUrl已经没有用了 // 没有登陆的用户只能访问登陆页面,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 //shiroFilterFactoryBean.setLoginUrl("/common/unauth"); // 登录成功后要跳转的链接 //shiroFilterFactoryBean.setSuccessUrl("/auth/index"); // 未授权界面; //shiroFilterFactoryBean.setUnauthorizedUrl("common/unauth"); //自定义拦截器 MapfiltersMap = new LinkedHashMap (); //自定义authc访问拦截器 filtersMap.put("authc", new AuthcShiroFilter()); //限制同一帐号同时在线的个数。 filtersMap.put("kickout", kickoutSessionControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); // 权限控制map. Map filterChainDefinitionMap = new LinkedHashMap (); // 公共请求 filterChainDefinitionMap.put("/common/**", "anon"); // 静态资源 filterChainDefinitionMap.put("/static/**", "anon"); // 登录方法 filterChainDefinitionMap.put("/admin/*ogin*", "anon"); // 表示可以匿名访问 filterChainDefinitionMap.put("/admin/sentCode", "anon"); // 表示可以匿名访问 filterChainDefinitionMap.put("/admin/query", "anon"); // 表示可以匿名访问 //此处需要添加一个kickout,上面添加的自定义拦截器才能生效 filterChainDefinitionMap.put("/admin/**", "authc,kickout");// 表示需要认证才可以访问 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.) */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setAuthenticator(modularRealmAuthenticator()); List realms = new ArrayList<>(); // 统一角色权限控制realm realms.add(authorizingRealm()); // 用户密码登录realm realms.add(userPasswordRealm()); // 用户手机号验证码登录realm realms.add(userPhoneRealm()); securityManager.setRealms(realms); // 自定义缓存实现 使用redis securityManager.setCacheManager(cacheManager()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 自定义的Realm管理,主要针对多realm */ @Bean("myModularRealmAuthenticator") public MyModularRealmAuthenticator modularRealmAuthenticator() { MyModularRealmAuthenticator customizedModularRealmAuthenticator = new MyModularRealmAuthenticator(); // 设置realm判断条件 customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return customizedModularRealmAuthenticator; } @Bean public AuthorizingRealm authorizingRealm(){ AuthorizationRealm authorizationRealm = new AuthorizationRealm(); authorizationRealm.setName(LoginType.COMMON.getType()); return authorizationRealm; } /** * 密码登录realm * * @return */ @Bean public UserPasswordRealm userPasswordRealm() { UserPasswordRealm userPasswordRealm = new UserPasswordRealm(); userPasswordRealm.setName(LoginType.USER_PASSWORD.getType()); // 自定义的密码校验器 userPasswordRealm.setCredentialsMatcher(credentialsMatcher()); return userPasswordRealm; } /** * 手机号验证码登录realm * @return */ @Bean public UserPhoneRealm userPhoneRealm(){ UserPhoneRealm userPhoneRealm = new UserPhoneRealm(); userPhoneRealm.setName(LoginType.USER_PHONE.getType()); return userPhoneRealm; } @Bean public CredentialsMatcher credentialsMatcher() { return new CredentialsMatcher(); } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识) redisCacheManager.setPrincipalIdFieldName("id"); redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:"); //设置前缀 return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:"); return redisSessionDAO; } /** * Session Manager * 使用的是shiro-redis开源插件 */ @Bean public SessionManager sessionManager() { SimpleCookie simpleCookie = new SimpleCookie("Token"); simpleCookie.setPath("/"); simpleCookie.setHttpOnly(false); SessionManager sessionManager = new SessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); sessionManager.setSessionIdCookieEnabled(false); sessionManager.setSessionIdUrlRewritingEnabled(false); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionIdCookie(simpleCookie); return sessionManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisHost); redisManager.setPort(redisPort); redisManager.setTimeout(1800); //设置过期时间 redisManager.setPassword(redisPassword); redisManager.setDatabase(database); return redisManager; } /** * 限制同一账号登录同时登录人数控制 * * @return */ // 这里的@Bean不要启用了,自定义的filter不要交由Spring创建,否则会出现被标记为anon的url仍然会执行该自定义过滤器。 updated 2020.06.05 //@Bean public SessionControlFilter kickoutSessionControlFilter() { SessionControlFilter kickoutSessionControlFilter = new SessionControlFilter(); kickoutSessionControlFilter.setCache(cacheManager()); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setKickoutAfter(false); kickoutSessionControlFilter.setMaxSession(1); kickoutSessionControlFilter.setKickoutUrl("/common/kickout"); return kickoutSessionControlFilter; } /*** * 授权所用配置 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /*** * 使授权注解起作用不如不想配置可以在pom文件中加入 * * * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * 此方法需要用static作为修饰词,否则无法通过@Value()注解的方式获取配置文件的值 * */ @Bean public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }}org.springframework.boot *spring-boot-starter-aop *
LoginType文件
package com.music.system.shiro.realm;public enum LoginType { /** * 通用 */ COMMON("common_realm"), /** * 用户密码登录 */ USER_PASSWORD("user_password_realm"), /** * 手机验证码登录 */ USER_PHONE("user_phone_realm"), /** * 第三方登录(微信登录) */ WECHAT_LOGIN("wechat_login_realm"); private String type; private LoginType(String type) { this.type = type; } public String getType() { return type; } @Override public String toString() { return this.type.toString(); }}
LoginController
/* Created by IntelliJ IDEA. User: Kalvin Date: 2020/10/26 Time: 12:29*/package com.music.controller;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.music.model.User;import com.music.service.IUserService;import com.music.service.impl.UserService;import com.music.system.enums.ResultStatusCode;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserToken;import com.music.system.vo.Result;import com.music.utils.JedisClient;import com.music.utils.OptionalLog;import com.music.utils.PhoneCode;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.LockedAccountException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.subject.Subject;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.util.Map;import static com.music.system.shiro.realm.LoginType.USER_PASSWORD;import static com.music.system.shiro.realm.LoginType.USER_PHONE;@CrossOrigin@RestController@RequestMapping("admin")public class LoginController { @Autowired private UserService userService; @Resource JedisClient jedisClient; /** * 用户密码登录 */ @RequestMapping("/login") public Result login(HttpServletRequest request){ String loginName = request.getParameter("name"); String password = request.getParameter("password"); request.getSession().setAttribute("loginName",loginName); System.out.println("我是登录页的"+request.getSession().getAttribute("loginName")); System.out.println(loginName+"---->"+password); UserToken token = new UserToken(LoginType.USER_PASSWORD, loginName, password); return shiroLogin(token,LoginType.USER_PASSWORD); } /** * 用户点击获取验证码请求 * */ @RequestMapping("/sentCode") public Result sentCode(@RequestBody Map map){ String phone = (String)map.get("phone"); System.out.println("phone----->"+phone); // PhoneCode.getPhonemsg(phone,jedisClient); //上线打开注释 return new Result(1); } /** * 手机验证码登录 */ @RequestMapping("/loginByPhone") public Result loginByPhone(@RequestBody Map map){ String phone = (String)map.get("phone"); String code = (String)map.get("code"); System.out.println(phone+"---->"+code); UserToken token = new UserToken(LoginType.USER_PHONE, phone, code); System.out.println("token---->"+token); return shiroLogin(token,LoginType.USER_PHONE); } @OptionalLog(modules="操作日志", methods="查询操作日志") @RequestMapping("/query") public void listLogInfo(){ System.out.println("我进入来了。。。。"); } public Result shiroLogin(UserToken token,LoginType loginType){ User user = null; String userName = null; String phone = null; try { //登录不在该处处理,交由shiro处理 Subject subject = SecurityUtils.getSubject(); System.out.println("subject-------->"+subject); if(LoginType.USER_PASSWORD.equals(loginType)){ userName = token.getUsername(); user = userService.findUserByName(userName); }else if(LoginType.USER_PHONE.equals(loginType)){ phone = token.getUsername(); user = userService.findUserByPhone(phone); } System.out.println(phone+"================="+userName); //出现异常 subject.login(token); if (subject.isAuthenticated()&&user!=null) { JSON json = new JSONObject(); ((JSONObject) json).put("token", subject.getSession().getId()); ((JSONObject) json).put("user",user); return new Result(ResultStatusCode.OK, json); }else{ return new Result(ResultStatusCode.SHIRO_ERROR); } }catch (IncorrectCredentialsException | UnknownAccountException e){ e.printStackTrace(); return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD); }catch (LockedAccountException e){ e.printStackTrace(); return new Result(ResultStatusCode.USER_FROZEN); }catch (Exception e){ e.printStackTrace(); return new Result(ResultStatusCode.SYSTEM_ERR); } }}
自定义多realm登录策略
package com.music.system.shiro;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserToken;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.pam.ModularRealmAuthenticator;import org.apache.shiro.realm.Realm;import java.util.Collection;import java.util.HashMap;/** * 自定义多realm登录策略 */public class MyModularRealmAuthenticator extends ModularRealmAuthenticator { @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判断getRealms()是否返回为空 assertRealmsConfigured(); // 所有Realm Collectionrealms = getRealms(); // 登录类型对应的所有Realm HashMap realmHashMap = new HashMap<>(realms.size()); for (Realm realm : realms) { realmHashMap.put(realm.getName(), realm); } UserToken token = (UserToken) authenticationToken; System.out.println("token------>"+token); // 登录类型 LoginType loginType = token.getLoginType(); if (realmHashMap.get(loginType.getType()) != null) { return doSingleRealmAuthentication(realmHashMap.get(loginType.getType()), token); } else { return doMultiRealmAuthentication(realms, token); } }}
JedisClient Java操作redis工具(可扩展)
/* Created by IntelliJ IDEA. User: Kalvin Date: 2020/5/13 Time: 14:21*/package com.music.utils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;@Componentpublic class JedisClient { @Autowired private StringRedisTemplate redisTemplate; // Key(键),简单的key-value操作 /** * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。 * * @param key * @return */ public long ttl(String key) { return redisTemplate.getExpire(key); } /** * 实现命令:expire 设置过期时间,单位秒 * * @param key * @return */ public void expire(String key, long timeout) { redisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 实现命令:INCR key,增加key一次 * * @param key * @return */ public long incr(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); } /** * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key */ public Setkeys(String pattern) { return redisTemplate.keys(pattern); } /** * 实现命令:DEL key,删除一个key * * @param key */ public void del(String key) { redisTemplate.delete(key); } // String(字符串) /** * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key) * * @param key * @param value */ public void set(String key, String value) { redisTemplate.opsForValue().set(key, value); } /** * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒) * * @param key * @param value * @param timeout * (以秒为单位) */ public void set(String key, String value, long timeout) { redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } /** * 实现命令:GET key,返回 key所关联的字符串值。 * * @param key * @return value */ public String get(String key) { return (String)redisTemplate.opsForValue().get(key); } // Hash(哈希表) /** * 实现命令:HEXISTS key field,查找哈希表中是否包含指定键值对 key中给定域 field的值 * @param key * @param field * @return */ public Boolean hexists(String key, String field){ return redisTemplate.opsForHash().hasKey(key, field); } /** * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value * * @param key * @param field * @param value */ public void hset(String key, String field, Object value) { redisTemplate.opsForHash().put(key, field, value); } /** * 实现命令:HGET key field,返回哈希表 key中给定域 field的值 * * @param key * @param field * @return */ public String hget(String key, String field) { return (String) redisTemplate.opsForHash().get(key, field); } /** * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 * * @param key * @param fields */ public void hdel(String key, Object... fields) { redisTemplate.opsForHash().delete(key, fields); } /** * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。 * * @param key * @return */ public Map
有关使用阿里云手机发送短信验证码,下篇文章详细介绍。这里只是分享核心代码,帮助理解记忆。
有兴趣的小伙伴可以关注微信公众号:幽灵邀请函 回复:网欲音乐 获取更多项目资料及完整项目,一起学习可以加QQ:2817670312
转载地址:http://inuzi.baihongyu.com/