shrio+jwt实现登录验证
1.导入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
2.创建shrio的config类,主要实现以下几点
- 配置过滤器(这里用的jwt)
- 设置Realm,这里我们没有设置特定的算法
package com.simplecode.service.config; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSessionStorageEvaluator; import org.apache.shiro.mgt.DefaultSubjectDAO; 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.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 org.springframework.context.annotation.DependsOn; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.apache.shiro.mgt.SecurityManager; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; @Slf4j @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //添加Shiro过滤器 /** * Shiro内置过滤器,可以实现权限相关的拦截器 * 常用的过滤器: * anon: 无需认证(登录)可以访问 * authc: 必须认证才可以访问 * user: 如果使用rememberMe的功能可以直接访问 * perms: 该资源必须得到资源权限才可以访问 * role: 该资源必须得到角色权限才可以访问 */ // 在 Shiro过滤器链上加入 自定义过滤器JWTFilter 并取名为jwt LinkedHashMap<String, Filter> filters = new LinkedHashMap<>(); filters.put("jwt", new JWTFilter()); // 拦截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/static/**", "anon"); // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/service/users/logout", "logout"); filterChainDefinitionMap.put("/service/users/login", "anon"); filterChainDefinitionMap.put("/service/users/user", "anon"); // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了; // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> filterChainDefinitionMap.put("/**", "jwt"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/service/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/service/index"); shiroFilterFactoryBean.setFilters(filters); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public ShiroRealm myShiroRealm() { return new ShiroRealm(); } @Bean public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm") ShiroRealm jwtRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // 设置realm manager.setRealm(jwtRealm); /** * 禁止session持久化存储 * 一定要禁止session持久化。不然清除认证缓存、授权缓存后,shiro依旧能从session中读取到认证信息 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } /** * 下面的代码是添加注解支持 */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { // 设置代理类 DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * * @param securityManager * @return // */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } // Shiro生命周期处理器 @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); // 数据库异常处理 mappings.setProperty("UnauthorizedException", "403"); r.setExceptionMappings(mappings); // None by default r.setDefaultErrorView("error"); // No default r.setExceptionAttribute("ex"); // Default is "exception" //r.setWarnLogCategory("example.MvcLogger"); // No default return r; } }
3.实现Realm,主要是认证与授权
- Authentication 相关的方法是认证
- Authorization 相关方法是授权
package com.simplecode.service.config; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.simplecode.service.entity.*; import com.simplecode.service.service.UserRoleRelationService; import com.simplecode.service.service.UsersService; import org.apache.commons.lang3.StringUtils; 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 org.apache.shiro.util.ByteSource; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.yaml.snakeyaml.scanner.Constant; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import java.util.Objects; @Configuration @MapperScan("com.simplecode.service.mapper") public class ShiroRealm extends AuthorizingRealm { @Autowired private RedisUtil redisUtil; @Resource private UsersService usersService; @Resource private UserRoleRelationService userRoleRelationService; // 必须重写此方法,不然Shiro会报错 @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 能进入这里说明用户已经通过验证了 Users users = (Users) principalCollection.getPrimaryPrincipal(); Long userId = users.getUserId(); List<UserRoleRelation> UserRoleRelations = userRoleRelationService.findRolesByUserId(userId); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); for (UserRoleRelation UserRoleRelation : UserRoleRelations) { Integer roleId = UserRoleRelation.getRoleId(); // simpleAuthorizationInfo.addRole(role.getRoleName()); // for (Permission permission : role.getPermissions()) { // simpleAuthorizationInfo.addStringPermission(permission.getPermissionName()); // } } return simpleAuthorizationInfo; } public static HttpServletRequest getHttpServletRequest() { return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String token = (String) authenticationToken.getCredentials(); String username = JwtUtil.getUsername(token); //从token中获取username Integer userId = JwtUtil.getUserId(token); //从token中获取userId // 通过redis查看token是否过期 HttpServletRequest request = getHttpServletRequest(); String encryptTokenInRedis = redisUtil.get("Constant.RM_TOKEN_CACHE" + token + StringPool.UNDERSCORE); if (!token.equalsIgnoreCase(encryptTokenInRedis)) { throw new AuthenticationException("token已经过期"); } // 如果找不到,说明已经失效 if (StringUtils.isBlank(encryptTokenInRedis)) { throw new AuthenticationException("token已经过期"); } if (StringUtils.isBlank(username)) { throw new AuthenticationException("token校验不通过"); } // 通过用户id查询用户信息 Users user = usersService.getById(userId); if (user == null) { throw new AuthenticationException("用户名或密码错误"); } if (!JwtUtil.verify(token, username, user.getUserPassword())) { throw new AuthenticationException("token校验不通过"); } return new SimpleAuthenticationInfo(token, token, "febs_shiro_realm"); } }
4.重写JWTFilter
- 调用流程:preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin
package com.simplecode.service.config; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.boot.configurationprocessor.json.JSONException; import org.springframework.boot.configurationprocessor.json.JSONObject; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import static com.simplecode.service.config.MGTConstants.TOKEN; @Slf4j public class JWTFilter extends BasicHttpAuthenticationFilter { /** * 对跨域提供支持 */ @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); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException { if (isLoginAttempt(request, response)) { return executeLogin(request, response); } return false; } /** * 判断用户是否想要登入。 * 检测header里面是否包含Authorization字段即可 */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader(TOKEN); return token != null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(TOKEN); //得到token JwtToken jwtToken = new JwtToken(token); // 解密token try { // 提交给realm进行登入,如果错误他会抛出异常并被捕获 getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回true return true; } catch (Exception e) { log.error(e.getMessage()); return false; } } @Override protected boolean sendChallenge(ServletRequest request, ServletResponse response) { log.debug("Authentication required: sending 401 Authentication challenge response."); HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setCharacterEncoding("utf-8"); httpResponse.setContentType("application/json; charset=utf-8"); final String message = "未认证,请在前端系统进行认证"; final Integer status = 401; try (PrintWriter out = httpResponse.getWriter()) { JSONObject responseJson = new JSONObject(); responseJson.put("msg", message); responseJson.put("status", status); out.print(responseJson); } catch (IOException | JSONException e) { log.error("sendChallenge error:", e); } return false; } }
5.JWTToken类以及JWTUtil
package com.simplecode.service.config; 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.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.crypto.hash.SimpleHash; import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.HashMap; import java.util.Map; import static com.simplecode.service.config.MGTConstants.TOKEN; @Slf4j public class JwtUtil { /** * 校验 token是否正确 * * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); verifier.verify(token); return true; } catch (Exception e) { log.info("token is invalid{}", e.getMessage()); return false; } } public static String getUsername(HttpServletRequest request) { // 取token String token = request.getHeader(TOKEN); return getUsername(token); } /** * 从 token中获取用户名 * @return token中包含的用户名 */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { log.error("error:{}", e.getMessage()); return null; } } public static Integer getUserId(HttpServletRequest request) { // 取token String token = request.getHeader(TOKEN); return getUserId(token); } /** * 从 token中获取用户ID * @return token中包含的ID */ public static Integer getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return Integer.valueOf(jwt.getSubject()); } catch (JWTDecodeException e) { log.error("error:{}", e.getMessage()); return null; } } /** * 生成 token * @param username 用户名 * @param secret 用户的密码 * @return token 加密的token */ public static String sign(String username, String secret, Long userId) { try { Map<String, Object> map = new HashMap<>(); map.put("alg", "HS256"); map.put("typ", "JWT"); username = StringUtils.lowerCase(username); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withHeader(map) .withClaim("username", username) .withSubject(String.valueOf(userId)) .withIssuedAt(new Date()) // .withExpiresAt(date) .sign(algorithm); } catch (Exception e) { log.error("error:{}", e); return null; } } public static String encrypt(String var){ return new SimpleHash("md5",var,"SALT".getBytes(),2).toHex(); } }
package com.simplecode.service.config; import lombok.Data; import org.apache.shiro.authc.AuthenticationToken; @Data public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; private String expireAt; public JwtToken(String token) { this.token = token; } public JwtToken(String token, String expireAt) { this.token = token; this.expireAt = expireAt; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
6.登录的controller
这里登录与用户信息获取分开成了两个接口。
package com.simplecode.service.controller; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.simplecode.common.utils.AESUtils; import com.simplecode.common.utils.SDResponse; import com.simplecode.service.config.JwtUtil; import com.simplecode.service.config.RedisUtil; import com.simplecode.service.entity.Users; import com.simplecode.service.service.UsersService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpRequest; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.servlet.support.RequestContext; import java.util.Objects; import static com.simplecode.service.config.MGTConstants.REDIS_SESSION_TIMEOUT; import static com.simplecode.service.config.MGTConstants.TOKEN; /** * <p> * 前端控制器 * </p> * * @author testjava * @since 2021-03-14 */ @Slf4j @CrossOrigin @RestController @RequestMapping("/service/users") public class UsersController { @Autowired private RedisUtil redisUtil; @Autowired UsersService usersService; @PostMapping("login") public SDResponse login (@RequestBody(required = true) Users users){ String userName = users.getUserName(); String userPassword = users.getUserPassword(); Users userEntity = null; try { userEntity = usersService.findByUsername(userName); } catch (Exception e){ log.error(e.getMessage()); return SDResponse.error().message(e.getMessage()); } if (!verifyPassword(userPassword, userEntity.getUserPassword())){ return SDResponse.error().message("username or password incorrect!"); } String token = JwtUtil.sign(userName, userEntity.getUserPassword(), userEntity.getUserId()); redisUtil.set("Constant.RM_TOKEN_CACHE" + token + StringPool.UNDERSCORE, token, REDIS_SESSION_TIMEOUT); return SDResponse.ok().data(TOKEN, token).data("users", userEntity); } @GetMapping("info") public SDResponse info(@RequestParam(required = true) String token){ Integer userId = JwtUtil.getUserId(token); Users user = usersService.findUserById(userId); return SDResponse.ok().data("users", user); } @PutMapping("user") public SDResponse register(@RequestBody(required = true) Users users){ String userName = users.getUserName(); String userPassword = users.getUserPassword(); if (userName.isEmpty() || userPassword.isEmpty()){ return SDResponse.error().message("username or password can not be empty"); } users.setUserPassword(AESUtils.AESEncode(userPassword)); try{ usersService.save(users);} catch (Exception e){ log.error(e.getMessage()); return SDResponse.error().message(e.getMessage()); } return SDResponse.ok().message("success"); } private boolean verifyPassword(String userPassword, String encryptPassword){ return Objects.equals(AESUtils.AESDecode(encryptPassword), userPassword); } }
7.前端请求
Login({ commit }, userInfo) { const username = userInfo.username.trim() return new Promise((resolve, reject) => { login(username, userInfo.password).then(response => { const data = response.data setToken(data.Authorization) commit('SET_TOKEN', data.Authorization) resolve() }).catch(error => { reject(error) }) }) },
Login方法调用后端登录接口后将token设置到全局变量中,方便全局设置header
// request拦截器 service.interceptors.request.use( config => { if (store.getters.token) { config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) } )
进一寸有进一寸的欢喜。