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)
  }
)

 

posted @ 2021-06-06 23:20  有虫子啊  阅读(313)  评论(0编辑  收藏  举报