SpringMVC + Shiro integram oauth2

SpringMVC + Shiro integram oauth2

Sobre implementação do cliente (sistema downstream de destino) e análise de problemas propensos a erros

Atualmente, muitas plataformas abertas, como a Sina Weibo Open Platform, estão usando interfaces API abertas para uso dos desenvolvedores, o que traz o problema de aplicativos de terceiros terem que ir para a plataforma aberta para autorização. OAuth faz isso, e OAuth2 é o Protocolo OAuth. Na próxima versão, em comparação com o OAuth1, todo o processo de autorização do OAuth2 é mais simples e seguro, mas não é compatível com o OAuth1. Para obter detalhes, você pode verificar o site oficial do OAuth2 em http://oauth.net /2/. Para a especificação do protocolo OAuth2, consulte http://tools.ietf.org/html/rfc6749. Atualmente existem muitas implementações de referência para escolher, que podem ser visualizadas e baixadas em seu site oficial.

Este artigo usa Apache Oltu, cujo nome anterior era Apache Amber, que é a implementação de referência da versão Java. Para documentação de uso, consulte https://cwiki.apache.org/confluence/display/OLTU/Documentation.

Função OAuth

Proprietário do recurso: uma entidade que pode autorizar o acesso a recursos protegidos, que pode ser uma pessoa, então o chamamos de usuário final; como o usuário do Sina Weibo zhangsan; servidor de recursos: armazena recursos protegidos, o cliente solicita recursos por meio do token de acesso
, e o servidor de recursos responde com recursos protegidos ao cliente; ele armazena informações como o Weibo do usuário zhangsan.
Servidor de autorização: Após verificar com sucesso o proprietário do recurso e obter autorização, o servidor de autorização emite um token de autorização (Token de Acesso) para o cliente.
Cliente: aplicativos de terceiros, como o cliente Sina Weibo weico e WeiGe, ou seu próprio aplicativo oficial; ele não armazena recursos em si, mas usa sua autorização após o proprietário do recurso autorizá-lo (token de autorização) para acessar recursos protegidos e, em seguida, o cliente exibe/envia os dados correspondentes ao servidor. O termo “cliente” não se refere a nenhuma implementação específica (como a aplicação executada em um servidor, desktop, telefone celular ou outro dispositivo).

Processo do protocolo OAuth2

insira a descrição da imagem aqui

1. O cliente solicita autorização do proprietário do recurso. As solicitações de autorização podem ser enviadas diretamente ao proprietário do recurso ou indiretamente por meio de um intermediário, como um servidor de autorização, que é o preferido.
2. O cliente recebe uma licença de autorização, que representa a autorização fornecida pelo servidor de recursos.
3. O cliente usa seu próprio certificado privado e licença de autorização para verificar com o servidor de autorização.
4. Se a verificação for bem-sucedida, um token de acesso será emitido.
5. O cliente utiliza o token de acesso para solicitar recursos protegidos do servidor de recursos.
6. O servidor de recursos verificará a validade do token de acesso e, se for bem-sucedido, emitirá recursos protegidos.

Para obter mais explicações sobre o processo, consulte a especificação do protocolo OAuth2 http://tools.ietf.org/html/rfc6749.

Sobre a implementação do cliente

Processo do cliente: se você precisar fazer login, primeiro vá para o servidor oauth2 para autorização de login. Após o sucesso, o servidor retorna o código de autenticação e, em seguida, o cliente usa o código de autenticação para ir ao servidor e trocar pelo token de acesso. é melhor obter informações do usuário com base no token de acesso para login do cliente.

Dependência de POM

Aqui usamos a implementação do cliente Apache Oltu Oauth2.
Código Java

<dependency>  
  <groupId>org.apache.oltu.oauth2</groupId>  
  <artifactId>org.apache.oltu.oauth2.client</artifactId>  
  <version>0.31</version>  
</dependency>  

Para outros, consulte pom.xml.

Crie uma entidade para armazenar Token

Se houver uma entidade UsernamePasswordToken no sistema original que armazena nome de usuário, senha e outras informações durante o processo de login, ela poderá ser modificada nela.

Semelhante a UsernamePasswordToken e CasToken; usado para armazenar o código de autenticação retornado pelo servidor oauth2.
Código Java

public class OAuth2Token implements AuthenticationToken {  
    private String authCode;  
    private String principal;  
    public OAuth2Token(String authCode) {  
        this.authCode = authCode;  
    }  
    //省略getter/setter  
}   

Código modificado em UsernamePasswordToken

@Getter
@Setter
public class UserNamePassWordRunAsToken extends UsernamePasswordToken {
    private static final long serialVersionUID = 2258294415444231569L;
    /**
     * 是否模拟登录
     */
    private Boolean runAs;

    private String authCode;

    private String principal;

    public UserNamePassWordRunAsToken() {
        super();
    }

    public UserNamePassWordRunAsToken(final String username, final String password, final Boolean runAs) {
        super(username, password);
        this.runAs = runAs;
    }

    public UserNamePassWordRunAsToken(String authCode) {
        this.authCode = authCode;
    }
}

Filtro OAuth2Authentication

A função deste filtro é semelhante ao FormAuthenticationFilter usado para controle de autenticação no cliente oauth2; se o usuário atual ainda não foi autenticado, ele primeiro determinará se existe um código na URL (o código de autenticação retornado pelo servidor) , e se não, redirecione para o servidor. Faça login e autorize e, em seguida, retorne o código de autenticação; então OAuth2AuthenticationFilter usará o código de autenticação para criar um OAuth2Token e, em seguida, envie-o para Subject.login para login; então OAuth2Realm executará o lógica de login correspondente baseada no OAuth2Token.
Código Java

@Setter
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {  
    //oauth2 authc code参数名  
    private String authcCodeParam = "code";  
    //客户端id  
    private String clientId;  
    //服务器端登录成功/失败后重定向到的客户端地址  
    private String redirectUrl;  
    //oauth2服务器响应类型  
    private String responseType = "code";  
    private String failureUrl;  
    //省略setter  
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {  
        HttpServletRequest httpRequest = (HttpServletRequest) request;  
        String code = httpRequest.getParameter(authcCodeParam);  
        return new OAuth2Token(code);  
        /*
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //请求路径
        String requestUrl = httpServletRequest.getRequestURL().toString();
        //设置返回请求
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;character=utf-8");
        //判断session是否已有
        HttpSession session = httpServletRequest.getSession();
        Object scuser = null;
        UserAuthController userAuthController;
        if (session != null) {
            scuser = session.getAttribute("scuser");
        }
        String code = httpServletRequest.getParameter("code");
        return new UserNamePassWordRunAsToken(code);
        */
    }  
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {  
        return false;  
    }  
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {  
        String error = request.getParameter("error");  
        String errorDescription = request.getParameter("error_description");  
        if(!StringUtils.isEmpty(error)) {//如果服务端返回了错误  
            WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);  
            return false;  
        }  
        Subject subject = getSubject(request, response);  
        if(!subject.isAuthenticated()) {  
            if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) {  
                //如果用户没有身份验证,且没有auth code,则重定向到服务端授权  
                saveRequestAndRedirectToLogin(request, response);  
                return false;  
            }  
        }  
        //执行父类里的登录逻辑,调用Subject.login登录  
        return executeLogin(request, response);  
    }  

    //登录成功后的回调方法 重定向到成功页面  
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,  ServletResponse response) throws Exception {  
        issueSuccessRedirect(request, response);  
        return false;  
    }  

    //登录失败后的回调   
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,  
                                     ServletResponse response) {  
        Subject subject = getSubject(request, response);  
        if (subject.isAuthenticated() || subject.isRemembered()) {  
            try { //如果身份验证成功了 则也重定向到成功页面  
                issueSuccessRedirect(request, response);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        } else {  
            try { //登录失败时重定向到失败页面  
                WebUtils.issueRedirect(request, response, failureUrl);  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
        return false;  
    }  
}   

A função deste interceptador:
1. Primeiro determine se existe um parâmetro de erro retornado pelo servidor. Em caso afirmativo, redirecione diretamente para a página de falha;
2. Em seguida, se o usuário ainda não tiver se autenticado, determine se existe um parâmetro de código de autenticação (ou seja, se é um serviço (retornado após autorização do cliente), caso contrário, redirecione para o servidor para autorização; 3. Caso contrário,
chame executeLogin para efetuar login, crie um OAuth2Token através do código de autenticação e envie-o ao Assunto para login;
4. Se o login for bem sucedido, o método de retorno de chamada onLoginSuccess irá redirecionar para a página de sucesso;
5. Se o login falhar, retornar de chamada onLoginFailure e redirecionar para a página de falha.

OAuth2Realm

Este Realm primeiro suporta apenas token do tipo OAuth2Token; em seguida, troca o código de autenticação recebido pelo token de acesso; em seguida, obtém informações do usuário (nome de usuário) com base no token de acesso e, em seguida, cria AuthenticationInfo com base nessas informações; se as informações de AuthorizationInfo forem necessárias, você pode use isto O nome de usuário obtido aqui é então obtido de acordo com suas próprias regras de negócios. Pode ser modificado na classe OAuth2Realm original do sistema.

Código Java

@Setter
public class OAuth2Realm extends AuthorizingRealm {  
    private String clientId;  
    private String clientSecret;  
    private String accessTokenUrl;  
    private String userInfoUrl;  
    private String redirectUrl;  
    //省略setter  
    public boolean supports(AuthenticationToken token) {  
        return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token类型  
    }  
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();  
        return authorizationInfo;  
    }  
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
        OAuth2Token oAuth2Token = (OAuth2Token) token;  
        String code = oAuth2Token.getAuthCode(); //获取 auth code  
        String username = extractUsername(code); // 提取用户名  
        SimpleAuthenticationInfo authenticationInfo =  
                new SimpleAuthenticationInfo(username, code, getName());  
        return authenticationInfo;  
    }  
    private String extractUsername(String code) {  
        try {  
            OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());  
            OAuthClientRequest accessTokenRequest = OAuthClientRequest  
                    .tokenLocation(accessTokenUrl)  
                    .setGrantType(GrantType.AUTHORIZATION_CODE)  
                    .setClientId(clientId).setClientSecret(clientSecret)  
                    .setCode(code).setRedirectURI(redirectUrl)  
                    .buildQueryMessage();  
            //获取access token  
            OAuthAccessTokenResponse oAuthResponse =   
                oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);  
            String accessToken = oAuthResponse.getAccessToken();  
            Long expiresIn = oAuthResponse.getExpiresIn();  
            //获取user info  
            OAuthClientRequest userInfoRequest =   
                new OAuthBearerClientRequest(userInfoUrl)  
                    .setAccessToken(accessToken).buildQueryMessage();  
            OAuthResourceResponse resourceResponse = oAuthClient.resource(  
                userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);  
            String username = resourceResponse.getBody();  
            return username;  
        } catch (Exception e) {  
            throw new OAuth2AuthenticationException(e);  
        }  
    }  
}  

Código modificado na classe OAuth2Realm original do sistema (método doGetAuthenticationInfo modificado). Pode referir-se a

protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token) throws AuthenticationException {
        final UserNamePassWordRunAsToken runAsToken = (UserNamePassWordRunAsToken) token;
        final Boolean runAs = null != runAsToken.getRunAs() ? runAsToken.getRunAs() :  false;

        String code = runAsToken.getAuthCode(); //获取 auth code
        final String loginName = extractUsername(code); // 提取用户名

        final User loginUser = new User();
        loginUser.setDeleted(Boolean.FALSE);
        loginUser.setLoginName(loginName);

        final User user = userService.getUser(loginUser);
        loginUser.setPassword(user.getPassword());

        runAsToken.setUsername(loginName);
        runAsToken.setPassword(user.getPassword().toCharArray());
        if (!runAs) {
            if (ObjectUtil.isNull(user)) {
                throw new UnknownAccountException();
            }
            if (user.getBizStatus().equals(BizStatusEnum.NOT_ENABLED)) {
                throw new NotEnabledAccountException();
            }
            if (user.getBizStatus().equals(BizStatusEnum.DISABLE)) {
                throw new DisabledAccountException();
            }
        } else {
            final User condUser = new User();
            condUser.setId(AuthConsts.ADMIN_ID);
            final User adminUser = userService.getUser(condUser);
            if (ObjectUtil.isNotNull(adminUser)) {
                user.setAdminName(adminUser.getLoginName());
                user.setAdminPassword(adminUser.getPassword());
            }
        }
        if (UserTypeEnum.SUPER_ADMIN.equals(user.getType())) {
            // 防止数据库中修改系统管理员属性
            user.setSysPosition(SysPositionEnum.NOTHING);
            user.setOrganizationId(null);
            user.setOrganization(null);
            user.setDepartmentId(null);
            user.setDepartment(null);
        } else {
            List<Long> organizationShareIds = CollUtil.newArrayList();
            final OrganizationShare condOrganizationShare = new OrganizationShare();
            condOrganizationShare.setOrganizationId(user.getOrganizationId());

            final List<OrganizationShare> organizationShareList = organizationShareService.listOrganizationShare(condOrganizationShare);
            if (CollUtil.isNotEmpty(organizationShareList)) {
                organizationShareIds = organizationShareList.stream().map(OrganizationShare::getOrganizationShareId).distinct().collect(Collectors.toList());
            }
            user.setOrganizationShareIds(organizationShareIds);
        }
        final SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, loginUser.getPassword(), getName());
        return info;
    }

Spring shiro配置(spring-config-shiro.xml)

Código Java

<bean id="oAuth2Realm"   
    class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">  
  <property name="cachingEnabled" value="true"/>  
  <property name="authenticationCachingEnabled" value="true"/>  
  <property name="authenticationCacheName" value="authenticationCache"/>  
  <property name="authorizationCachingEnabled" value="true"/>  
  <property name="authorizationCacheName" value="authorizationCache"/>  
  <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>  
  <property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>  
  <property name="accessTokenUrl"  value="http://localhost:8080/chapter17-server/accessToken"/>  
  <property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>  
  <property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>  
</bean>   

Configuração Spring Shiro modificada na classe oAuth2Realm do sistema original, apenas para referência
http://localhost:9080/chapter17-client/oauth2-login é o endereço do
código Java do sistema downstream de destino após login bem-sucedido

    <bean id="authRealm" class="com.csw.auth.realm.CswUserRealm">
        <property name="userService" ref="userService"/>
        <property name="permissionService" ref="permissionService"/>
        <property name="organizationShareService" ref="organizationShareService"/>
        <property name="authenticationTokenClass" value="com.csw.auth.realm.UserNamePassWordRunAsToken"/>
        <property name="cachingEnabled" value="true"/>
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="authenticationCache"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="authorizationCache"/>
        <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>  
        <property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>  
         <property name="accessTokenUrl"  value="http://localhost:8080/chapter17-server/accessToken"/>  
        <property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
        <property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
    </bean>

Se você estiver criando uma nova classe oAuth2Realm, preste atenção à configuração do securityManager
insira a descrição da imagem aqui
e configure
o código Java oAuth2AuthenticationFilter

<bean id="oAuth2AuthenticationFilter"   
    class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">  
  <property name="authcCodeParam" value="code"/>  
  <property name="failureUrl" value="/oauth2Failure.jsp"/>  
</bean> 

Este OAuth2AuthenticationFilter é usado para interceptar o código de autenticação redirecionado de volta do servidor.

Código Java

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
  <property name="securityManager" ref="securityManager"/>  
  <property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>  
  <property name="successUrl" value="/"/>  
  <property name="filters">  
      <util:map>  
         <entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>  
      </util:map>  
  </property>  
  <property name="filterChainDefinitions">  
      <value>  
          / = anon  
          /oauth2Failure.jsp = anon  
          /oauth2-login = oauth2Authc  
          /logout = logout  
          /** = user  
      </value>  
  </property>  
</bean>  

Defina loginUrl aqui para http://localhost:8080/chapter17-server/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"; será automaticamente definido para todos os AccessControlFilters, como oAuth2AuthenticationFilter; além disso, /oauth2-login = oauth2Authc significa que o endereço /oauth2-login usa o interceptor oauth2Authc para interceptar e executar a autorização do cliente oauth2.

Todas as configurações agora estão concluídas

sugestão

É melhor não usar o URL após o login bem-sucedido do sistema downstream de destino como o endereço de retorno de chamada, porque será o mesmo que o URL que o sistema vai para a página inicial após a verificação do filtro ser bem-sucedida, fazendo com que o filtro seja verificado novamente, e quando for verificado novamente, porque O sistema salta sozinho, fazendo com que a verificação do filtro falhe e retorne à interface do portal unificado.

A solução é: criar uma URL especial para login integrado e utilizá-la para identificação do filtro. Após a identificação bem-sucedida, vá para a página inicial para evitar a verificação do filtro novamente.

Por exemplo:
a url após o login bem-sucedido do sistema original é: http://192.168.11.54:8080/main.Definido
para criar uma url especial para login integrado: http://192.168.11.54:8080/userAuth/oAuth2;

@RestController
@RequestMapping(SystemConsts.UserAuth.CONTROLLER)
public class UserAuthController extends BaseCswController {
     //其他代码省略
       
     //设置为集成登录创建一个专门的url
     @GetMapping(path = "/oAuth2")
	 public String oAuth2() {
		return SystemConsts.Main.MAIN;
	 }
}
@Controller
@RequestMapping(SystemConsts.Main.CONTROLLER)
public class MainController {
    @GetMapping()
    public String index() {
        final User loginUser = AuthUserUtils.getUser();
        HttpUtils.getRequest().getSession().setAttribute(SystemConsts.KEY_SESSION_USER, loginUser);
        return SystemConsts.Main.MAIN;
    }
}

Ajustando a configuração do OAuth2AuthenticationFilter

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="http://192.168.7.61:8882/portal/login.html?client_id=APP015&amp;response_type=code&amp;redirect_uri=http://192.168.11.54:8080/main"/>
        <property name="successUrl" value="http://192.168.11.54:8080/main"/>
        <property name="unauthorizedUrl" value="/"/>
        <property name="filters">
            <util:map>
                <entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /userAuth/login = anon
                /favicon.ico = anon
                /userAuth/oAuth2 = oauth2Authc   //做修改的地方
                /static/** = anon
                /** = authc
            </value>
        </property>
    </bean>

Referência original: https://blog.csdn.net/qq_32347977/article/details/51093895

Acho que você gosta

Origin blog.csdn.net/qq_38696286/article/details/120749004
Recomendado
Clasificación