Аутентификация авторитета интерфейса на основе JWT и Shiro

Безопасность

Цели

  • После того, как пользователь войдет в систему, он может использовать токен для доступа к некоторым интерфейсам, требующим входа в систему.

Что такое JWT и Широ

JWT

JWT (веб-токены json) — это открытый стандарт на основе JSON ((RFC 7519), решение для междоменной аутентификации.

сочинение

JSON Web Token состоит из трех частей, соединенных точками (.). Три части:

  • Header
  • Payload
  • Signature

Header

Заголовок обычно состоит из двух частей: типа маркера («JWT») и имени алгоритма (например, HMAC SHA256 или RSA и т. д.).

Например:

{
  "typ": "JWT",
  "alg": "HS256"
}

Затем закодируйте этот JSON с помощью Base64, чтобы получить первую часть JWT.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

Полезная нагрузка содержит определенное содержимое маркера. Часть этого содержимого является стандартным полем, и вы также можете добавить другое необходимое содержимое. Ниже приведены стандартные поля:

  • iss: Эмитент, эмитент
  • суб: Тема, тема
  • ауди: Аудитория, аудитория
  • exp: Срок действия, срок действия
  • нбф: не раньше
  • iat: Дата выпуска, время выпуска
  • jti: идентификатор JWT Например, следующая полезная нагрузка использует эмитента iss и время истечения срока действия. Также есть два настраиваемых поля: одно name , а другое admin .
{
 "iss": "ninghao.net",
 "exp": "1438955445",
 "name": "wanghao",
 "admin": true
}

Используйте Base64 для кодирования этого JSON, чтобы получить вторую часть JWT.

eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ Signature

Signature

Последняя часть JWT — это Signature, которая состоит из трех частей, сначала закодированных с помощью Base64.header.payload, а затем зашифровать его с помощью алгоритма шифрования. При шифровании поставьте Secret , который эквивалентен паролю. Этот пароль тайно хранится на сервере. Процесс шифрования выглядит следующим образом:

String encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);

String token = HMACSHA256(encodedString, 'secret');

В это время получается третья часть токена:

SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

Наконец, эти три части используются.Объединение вместе — это наш токен для отправки клиенту:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

Shiro

Широ — это инфраструктура безопасности, которая в основном используется для аутентификации и авторизации.Здесь мы используем авторизацию для перехвата URL-адресов, чтобы определить, есть ли у пользователей разрешение на доступ.

Общая идея реализации

Сервер генерирует токен через JWT и отправляет его клиенту. После того, как клиент его получает, он помещает токен в заголовок запроса для каждого запроса. Shiro перехватывает каждый интерфейс. После перехвата токен в заголовке запроса вынимается для проверки JWT. Если проверка проходит, он освобождается. это не удается, клиенту будет сообщено об ошибке без разрешения.

Код

Создайте проект весенней загрузки, файл pom.xml выглядит следующим образом:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.domain</groupId>
    <artifactId>hello-jwt-shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>hello-jwt-shiro</name>
    <description>Demo project for Spring Boot with JWT and Shrio</description>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <shiro.spring.version>1.4.0</shiro.spring.version>
        <jwt.auth0.version>3.2.0</jwt.auth0.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 使用redis做数据缓存,如果不需要可不依赖 -->
        <!--<dependency>-->
            <!--<groupId>org.springframework.boot</groupId>-->
            <!--<artifactId>spring-boot-starter-data-redis</artifactId>-->
        <!--</dependency>-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>${shiro.spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${jwt.auth0.version}</version>
        </dependency>
        <!--<dependency>-->
            <!--<groupId>org.apache.httpcomponents</groupId>-->
            <!--<artifactId>httpclient</artifactId>-->
            <!--<version>4.5.5</version>-->
        <!--</dependency>-->
        <!--<dependency>-->
            <!--<groupId>org.apache.commons</groupId>-->
            <!--<artifactId>commons-lang3</artifactId>-->
            <!--<version>3.7</version>-->
        <!--</dependency>-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Профиль Широ

Установите фильтр Token и URL-адрес для фильтрации. Все необходимые отфильтрованные URL-адреса сначала будут проходить через фильтр, а фильтр пройдет проверку, прежде чем достигнет контроллера.

@Configuration
public class ShiroConfiguration {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("token", new TokenAccessFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/**", "token");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

реализация фильтра

Получите токен в заголовке запроса клиента и передайте его JWT для проверки и сравнения.Если он пройдет, он будет освобожден.После освобождения будет выполнен код контроллера.尚未登录.

public class TokenAccessFilter extends AccessControlFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        if(((HttpServletRequest) servletRequest).getMethod().equalsIgnoreCase("OPTIONS")) {
            return true;
        }
        return false;
    }


    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws BusinessException, IOException {
        String token = ((HttpServletRequest) servletRequest).getHeader("token");
        if (token==null || token.length()==0) {
            responseError(servletResponse,401,"尚未登录");
            return false;
        }
        String username = JWTUtil.getUsername(token);

        if (!JWTUtil.verify(token,username)) {
            responseError(servletResponse,401,"token 验证失败");
            return  false;
        }

        return true;
    }

    private void responseError(ServletResponse response,int code,String errorMsg) throws  IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-Control-Allow-Origin","*");
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");		httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");
        httpServletResponse.setContentType("application/json; charset=UTF-8");
        ResponseBean baseResponse = new ResponseBean(code,errorMsg,null);
        OutputStream os = httpServletResponse.getOutputStream();
        os.write(new ObjectMapper().writeValueAsString(baseResponse).getBytes("UTF-8"));
        os.flush();
        os.close();
    }
}

Генерация и проверка токена

public class JWTUtil {

    // 过期时间5分钟
    private static final long EXPIRE_TIME = 5*60*1000;

    private static final String SECRET = "XX#$%()(#*!()!KL<><MQLMNQNQJQK sdfkjsdrow32234545fdf>?N<:{LWPW";

    /**
     * 校验token是否正确
     * @param token 密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public static String sign(String username) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

Пользовательский мир

Пользовательская логика авторизации и аутентификации.

doGetAuthorizationInfoМетод аутентификации авторизации.doGetAuthenticationInfoМетод аутентификации

getSubject(request, response).login(token)Он передается этому пользовательскому классу для обработки.

@Service
public class MyRealm extends AuthorizingRealm {

    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

//    /**
//     * 大坑!,必须重写此方法,不然Shiro会报错
//     */
//    @Override
//    public boolean supports(AuthenticationToken token) {
//        return token instanceof JWTToken;
//    }

//    /**
//     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
//     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        UserBean user = userService.getUser(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole(user.getRole());
        Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        simpleAuthorizationInfo.addStringPermissions(permission);
        return simpleAuthorizationInfo;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserBean userBean = userService.getUser(username);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (! JWTUtil.verify(token, username)) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

Полный код проекта

GitHub.com/domain9065/…