• 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)
      }
    )
    进一寸有进一寸的欢喜。
  • 相关阅读:
    form表单的应用
    HTML列表及表格的基本应用
    Linux上安装Jdk
    docker+jenkins自动发布步骤及命令
    redis集群部署实战
    mySQL中连接字符串
    mysql触发器
    sql 设置数字长度 不足时补0
    微服务架构特性
    SQLServer2008 去除换行符
  • 原文地址:https://www.cnblogs.com/xu-xiaofeng/p/14856846.html
Copyright © 2020-2023  润新知