使用场景:用户使用手机号或密码登录后返回token,之后的请求的请求头带上token进行token鉴权。
一、依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-aopartifactId> dependency> <dependency> <groupId>com.auth0groupId> <artifactId>java-jwtartifactId> <version>3.4.0version> dependency> <dependency> <groupId>org.apache.shirogroupId> <artifactId>shiro-springartifactId> <version>${shiro.verison}version> dependency>
二、配置
package com.ydzl.lxyd.config; import com.ydzl.lxyd.config.realm.CodeRealm; import com.ydzl.lxyd.config.realm.JwtRealm; import com.ydzl.lxyd.config.realm.PasswordRealm; import com.ydzl.lxyd.config.realm.UserModularRealmAuthenticator; import com.ydzl.lxyd.filter.JwtFilter; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.*; /** * @author lixiao * @date 2019/7/31 11:34 */ @Configuration public class ShiroConfig { /** * 开启shiro权限注解 * @return DefaultAdvisorAutoProxyCreator */ @Bean public static DefaultAdvisorAutoProxyCreator creator(){ DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * * @param securityManager 安全管理器 * @return AuthorizationAttributeSourceAdvisor */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 密码登录时使用该匹配器进行匹配 * @return HashedCredentialsMatcher */ @Bean("hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); // 设置哈希算法名称 matcher.setHashAlgorithmName("MD5"); // 设置哈希迭代次数 matcher.setHashIterations(1024); // 设置存储凭证十六进制编码 matcher.setStoredCredentialsHexEncoded(true); return matcher; } @Bean public PasswordRealm passwordRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){ PasswordRealm userRealm = new PasswordRealm(); userRealm.setCredentialsMatcher(matcher); return userRealm; } @Bean public CodeRealm codeRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){ CodeRealm codeRealm = new CodeRealm(); codeRealm.setCredentialsMatcher(matcher); return codeRealm; } @Bean public JwtRealm jwtRealm(){ return new JwtRealm(); } /** * Shiro内置过滤器,可以实现拦截器相关的拦截器 * 常用的过滤器: * anon:无需认证(登录)可以访问 * authc:必须认证才可以访问 * user:如果使用rememberMe的功能可以直接访问 * perms:该资源必须得到资源权限才可以访问 * role:该资源必须得到角色权限才可以访问 **/ @Bean public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置 SecurityManager bean.setSecurityManager(securityManager); // 设置未登录跳转url bean.setLoginUrl("/user/unLogin"); MapfilterMap = new LinkedHashMap<>(); filterMap.put("/user/**","anon"); filterMap.put("/static/**","anon"); filterMap.put("/user/logout", "logout"); //从这里开始,是我为解决问题增加的,为swagger页面放行 filterMap.put("/swagger-ui.html", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/v2/**", "anon"); filterMap.put("/webjars/**", "anon"); Map filter = new HashMap<>(1); filter.put("jwt", new JwtFilter()); bean.setFilters(filter); filterMap.put("/**", "jwt"); bean.setFilterChainDefinitionMap(filterMap); return bean; } @Bean public UserModularRealmAuthenticator userModularRealmAuthenticator(){ //自己重写的ModularRealmAuthenticator UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return modularRealmAuthenticator; } /** * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.) */ @Bean public SecurityManager securityManager(@Qualifier("passwordRealm") PasswordRealm passwordRealm, @Qualifier("codeRealm") CodeRealm codeRealm, @Qualifier("jwtRealm") JwtRealm jwtRealm, @Qualifier("userModularRealmAuthenticator") UserModularRealmAuthenticator userModularRealmAuthenticator) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm securityManager.setAuthenticator(userModularRealmAuthenticator); List realms = new ArrayList<>(); // 添加多个realm realms.add(passwordRealm); realms.add(codeRealm); realms.add(jwtRealm); securityManager.setRealms(realms); /* * 关闭shiro自带的session,详情见文档 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } }
三、shiro多方式鉴权需要配置多个realm
1.验证码登录Realm
package com.ydzl.lxyd.config.realm; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.ydzl.lxyd.config.token.CustomizedToken; import com.ydzl.lxyd.pojo.user.User; import com.ydzl.lxyd.service.user.UserService; import com.ydzl.lxyd.util.JwtUtil; import com.ydzl.lxyd.util.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import javax.annotation.Resource; /** * @author lixiao * @date 2019/7/31 11:40 */ @Slf4j public class CodeRealm extends AuthorizingRealm { @Resource private UserService userService; @Resource private RedisUtil redisUtil; @Override public boolean supports(AuthenticationToken token) { return token instanceof CustomizedToken; } /** * 获取授权信息 * @param principals principals * @return AuthorizationInfo */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } /** * 获取身份认证信息 * @param authenticationToken authenticationToken * @return AuthenticationInfo * @throws AuthenticationException AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { CustomizedToken token = (CustomizedToken) authenticationToken; log.info("CodeRealm"+token.getUsername()+"开始身份认证"); // 根据手机号查询用户 User user = userService.selectOne(new EntityWrapper().eq("phone", token.getUsername())); if (user == null) { // 抛出账号不存在异常 throw new UnknownAccountException(); } // 1.principal:认证的实体信息,可以是手机号,也可以是数据表对应的用户的实体类对象 // 2.从redis中获取登录验证码 Object credentials = redisUtil.get(user.getPhone()+"loginCode"); // 3.realmName:当前realm对象的name,调用父类的getName()方法即可 // 4.盐,取用户信息中唯一的字段来生成盐值,避免由于两个用户原始密码相同,加密后的密码也相同 ByteSource credentialsSalt = ByteSource.Util.bytes(user.getPhone()); return new SimpleAuthenticationInfo(user, credentials, credentialsSalt, getName()); } }
2.密码登录Realm
package com.ydzl.lxyd.config.realm; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.ydzl.lxyd.config.token.CustomizedToken; import com.ydzl.lxyd.pojo.user.User; import com.ydzl.lxyd.service.user.UserService; import com.ydzl.lxyd.util.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import javax.annotation.Resource; /** * @author lixiao * @date 2019/7/31 11:40 */ @Slf4j public class PasswordRealm extends AuthorizingRealm { @Resource private UserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof CustomizedToken; } /** * 获取授权信息 * @param principals principals * @return AuthorizationInfo */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } /** * 获取身份认证信息 * @param authenticationToken authenticationToken * @return AuthenticationInfo * @throws AuthenticationException AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { CustomizedToken token = (CustomizedToken) authenticationToken; log.info("PasswordRealm"+ token.getUsername()+ "开始身份认证"); // 根据手机号查询用户 User user = userService.selectOne(new EntityWrapper().eq("phone", token.getUsername())); if (user == null) { // 抛出账号不存在异常 throw new UnknownAccountException(); } // 1.principal:认证的实体信息,可以是手机号,也可以是数据表对应的用户的实体类对象 // 2.credentials:密码 Object credentials = user.getPassword(); // 3.realmName:当前realm对象的name,调用父类的getName()方法即可 // 4.盐,取用户信息中唯一的字段来生成盐值,避免由于两个用户原始密码相同,加密后的密码也相同 ByteSource credentialsSalt = ByteSource.Util.bytes(user.getPhone()); return new SimpleAuthenticationInfo(user, credentials, credentialsSalt, getName()); } }
3.JwtTokenRealm
package com.ydzl.lxyd.config.realm; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.ydzl.lxyd.config.token.JwtToken; import com.ydzl.lxyd.enums.ResultEnums; import com.ydzl.lxyd.pojo.user.User; import com.ydzl.lxyd.service.user.UserService; import com.ydzl.lxyd.util.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import javax.annotation.Resource; /** * @author lixiao * @date 2019/8/6 10:02 */ @Slf4j public class JwtRealm extends AuthorizingRealm { @Resource private UserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("JwtRealm doGetAuthorizationInfo 运行()"); String username = JwtUtil.getPhone(principals.toString()); User user = userService.selectOne(new EntityWrapper().eq("phone", username)); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole(user.getLastLoginIdentity()==0?"user":"driver"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); // 解密获得username,用于和数据库进行对比 String username = JwtUtil.getPhone(token); if (username == null) { throw new AuthenticationException(ResultEnums.TOKEN_INVALID.getMsg()); } User user = userService.selectOne(new EntityWrapper ().eq("phone", username)); if(user == null){ throw new AuthenticationException(ResultEnums.USER_NOT_EXIST.getMsg()); } if (!JwtUtil.verify(token, username, user.getUserId(), user.getPassword())) { throw new AuthenticationException(ResultEnums.TOKEN_ERROR.getMsg()); } return new SimpleAuthenticationInfo(token, token, "JwtRealm"); } }
4.当配置了多个Realm时,我们通常使用的认证器是shiro自带的 org.apache.shiro.authc.pam.ModularRealmAuthenticator, 其中决定使用的Realm的是doAuthenticate()方法
package com.ydzl.lxyd.config.realm; import com.ydzl.lxyd.config.token.CustomizedToken; import com.ydzl.lxyd.config.token.JwtToken; import lombok.extern.slf4j.Slf4j; 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.ArrayList; import java.util.Collection; /** * @author lixiao * @date 2019/7/31 20:48 * 当配置了多个Realm时,我们通常使用的认证器是shiro自带的 * org.apache.shiro.authc.pam.ModularRealmAuthenticator, * 其中决定使用的Realm的是doAuthenticate()方法 */ @Slf4j public class UserModularRealmAuthenticator extends ModularRealmAuthenticator { @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("UserModularRealmAuthenticator:method doAuthenticate() 执行 "); // 判断getRealms()是否返回为空 assertRealmsConfigured(); // 所有Realm Collectionrealms = getRealms(); // 登录类型对应的所有Realm Collection typeRealms = new ArrayList<>(); // 强制转换回自定义的Token try{ JwtToken jwtToken = (JwtToken) authenticationToken; for(Realm realm : realms){ if (realm.getName().contains("Jwt")){ typeRealms.add(realm); } } return doSingleRealmAuthentication(typeRealms.iterator().next(), jwtToken); }catch (ClassCastException e){ typeRealms.clear(); CustomizedToken customizedToken = (CustomizedToken) authenticationToken; // 登录类型 String loginType = customizedToken.getLoginType(); for (Realm realm : realms) { if (realm.getName().contains(loginType)){ typeRealms.add(realm); } } // 判断是单Realm还是多Realm if(typeRealms.size() == 1){ return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken); }else { return doMultiRealmAuthentication(typeRealms, customizedToken); } } } }
四、其中用到了JWTUtils / 自定义TOKEN
package com.ydzl.lxyd.util; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.ydzl.lxyd.commons.Constant; import java.util.Date; /** * @author lixiao * @date 2019/8/5 23:46 */ public class JwtUtil { /** * 校验token是否正确 * * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token, String phone, Integer userId, String secret) { try { //根据密码生成JWT效验器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("phone", phone) .withClaim("userId", String.valueOf(userId)) .build(); // 效验TOKEN verifier.verify(token); return true; } catch (JWTVerificationException exception) { return false; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getPhone(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("phone").asString(); } catch (JWTDecodeException e) { return null; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userId").asString(); } catch (JWTDecodeException e) { return null; } } /** * * @param phone 用户名/手机号 * @param userId 用户id * @param secret 用户的密码 * @return 加密的token */ public static String sign(String phone,Integer userId, String secret) { Date date = new Date(System.currentTimeMillis() + Constant.TOKEN_EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("phone", phone) .withClaim("userId", String.valueOf(userId)) .withExpiresAt(date) .sign(algorithm); } }
package com.ydzl.lxyd.config.token; import org.apache.shiro.authc.UsernamePasswordToken; /** * @author lixiao * @date 2019/7/31 20:54 */ public class CustomizedToken extends UsernamePasswordToken { /** * 登录类型 */ private String loginType; public CustomizedToken(final String username, final String password, String loginType){ super(username, password); this.loginType = loginType; } public String getLoginType() { return loginType; } public void setLoginType(String loginType) { this.loginType = loginType; } @Override public String toString(){ return "loginType="+ loginType +",username=" + super.getUsername()+",password="+ String.valueOf(super.getPassword()); } }
package com.ydzl.lxyd.config.token; import org.apache.shiro.authc.AuthenticationToken; /** * @author lixiao * @date 2019/8/5 23:43 */ public class JwtToken implements AuthenticationToken { private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
六、自定义filter拦截自定义请求头拦截 / 获取token后进行鉴权
package com.ydzl.lxyd.filter; import com.ydzl.lxyd.commons.Constant; import com.ydzl.lxyd.config.token.JwtToken; import com.ydzl.lxyd.enums.ResultEnums; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author lixiao * @date 2019/8/5 23:46 */ @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { /** * 执行登录认证 * @param request ServletRequest * @param response ServletResponse * @param mappedValue mappedValue * @return 是否成功 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { return false; } } /** * 执行登录 */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response){ HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME); if (token == null) { try { request.getRequestDispatcher("/user/tokenError?msg="+ ResultEnums.TOKEN_NOT_EXIST.getMsg()).forward(request, response); } catch (ServletException | IOException e) { log.error("JwtFilter executeLogin() /user/tokenError"); } return false; } JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 try{ getSubject(request, response).login(jwtToken); } catch (AuthenticationException e){ try { request.getRequestDispatcher("/user/tokenError?msg="+e.getMessage()).forward(request, response); } catch (ServletException | IOException e1) { log.error("JwtFilter executeLogin() /user/tokenError"); } } // 如果没有抛出异常则代表登入成功,返回true return true; } /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
相关疑问请邮件联系:[email protected]