Spring MVC + Shiro integration oauth2

Spring MVC + Shiro integration oauth2

Analysis on the implementation of client (target downstream system) and error prone problems

At present, many open platforms, such as Sina Weibo open platform, are used to provide open API interfaces for developers to use, which brings the problem that third-party applications need to be authorized to the open platform. OAuth does this. OAuth2 is the next version of OAuth protocol. Compared with OAuth1, the whole authorization process of OAuth2 is simpler and safer, but it is not compatible with OAuth1, You can go to OAuth2 official website for details http://oauth.net/2/ See the OAuth2 protocol specification for reference http://tools.ietf.org/html/rfc6749 . At present, there are many reference implementations to choose from. You can view and download them on its official website.

This article uses Apache Oltu, formerly known as Apache Amber, which is a reference implementation for the Java version. Use the documentation for reference https://cwiki.apache.org/confluence/display/OLTU/Documentation .

OAuth role

resource owner: an entity that can authorize access to protected resources, which can be a person, we call it end user; For example, Sina Weibo user zhangsan;
Resource server: store protected resources, the client requests resources through access token, and the resource server responds to the protected resources to the client; It stores user zhangsan's microblog and other information.
Authorization server: after successfully verifying the resource owner and obtaining authorization, the authorization server issues an Access Token to the client.
Client: third party applications such as Sina Weibo client, weico and micro grid, or its own official applications; It does not store resources, but after the resource owner is authorized, it uses its authorization (authorization token) to access the protected resources, and then the client displays / submits the corresponding data to the server. The term "client" does not represent any specific implementation (such as an application running on a server, desktop, mobile phone or other device).

OAuth2 protocol process

1. The client requests authorization from the resource owner. The authorization request can be sent directly to the resource owner or indirectly through the intermediary of the authorization server, which is preferable.
2. The client receives an authorization license on behalf of the authorization provided by the resource server.
3. The client uses its own private certificate and authorization license to authenticate with the authorization server.
4. If the verification is successful, an access token is issued.
5. The client requests protected resources from the resource server using an access token.
6. The resource server will verify the validity of the access token and issue the protected resource if successful.

For more process explanations, please refer to the protocol specification of OAuth2 http://tools.ietf.org/html/rfc6749 .

Implementation of client

Client process: if you need to log in, first jump to oauth2 server for login authorization. After success, the server returns auth code, and then the client uses auth code to the server for access token. It is best to obtain user information according to access token to bind the login of the client.

POM dependency

Here we use the apache oltu oauth2 client implementation.
Java code

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

For others, please refer to pom.xml.

Create an entity to store tokens

If there is an entity UsernamePasswordToken in the original system that stores the user name, password and other information in the login process, it can be transformed on it.

Similar to UsernamePasswordToken and CasToken; Used to store oauth2 the auth code returned by the server.
Java code

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

Code modified on UsernamePasswordToken

@Getter
@Setter
public class UserNamePassWordRunAsToken extends UsernamePasswordToken {
    private static final long serialVersionUID = 2258294415444231569L;
    /**
     * Simulate login
     */
    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;
    }
}

OAuth2AuthenticationFilter

The function of this filter is similar to that of formauth2 client authentication filter; If the current user has not been authenticated, first judge whether there is a code (auth code returned by the server) in the url. If not, redirect to the server for login and authorization, and then return auth code; Then, OAuth2AuthenticationFilter will create an OAuth2Token with auth code and submit it to Subject.login for login; Then OAuth2Realm will perform corresponding login logic according to OAuth2Token.
Java code

@Setter
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {  
    //oauth2 authc code parameter name  
    private String authcCodeParam = "code";  
    //Client id  
    private String clientId;  
    //The client address to which the server will be redirected after successful / failed login  
    private String redirectUrl;  
    //oauth2 server response type  
    private String responseType = "code";  
    private String failureUrl;  
    //Omit 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;
        //Request path
        String requestUrl = httpServletRequest.getRequestURL().toString();
        //Set return request
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;character=utf-8");
        //Determine whether the session already exists
        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)) {//If the server returns an 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))) {  
                //If the user has no authentication and no auth code, it will be redirected to the server for authorization  
                saveRequestAndRedirectToLogin(request, response);  
                return false;  
            }  
        }  
        //Execute the login logic in the parent class and call Subject.login to login  
        return executeLogin(request, response);  
    }  

    //The callback method after successful login redirects to the success page  
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,  ServletResponse response) throws Exception {  
        issueSuccessRedirect(request, response);  
        return false;  
    }  

    //Callback after login failure   
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,  
                                     ServletResponse response) {  
        Subject subject = getSubject(request, response);  
        if (subject.isAuthenticated() || subject.isRemembered()) {  
            try { //If authentication is successful, you will also be redirected to the success page  
                issueSuccessRedirect(request, response);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        } else {  
            try { //Redirect to failure page when login fails  
                WebUtils.issueRedirect(request, response, failureUrl);  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
        return false;  
    }  
}   

The function of the Interceptor:
1. First, judge whether there is an error parameter returned by the server, and if so, redirect directly to the failure page;
2. Then, if the user has not been authenticated, judge whether there is auth code parameter (that is, whether it is returned after the server authorization). If not, redirect to the server for authorization;
3. Otherwise, call executeLogin to log in, create OAuth2Token through auth code and submit it to Subject for login;
4. After successful login, redirect the callback onLoginSuccess method to the success page;
5. If the login fails, the callback onLoginFailure redirects to the failure page.

OAuth2Realm

Firstly, this Realm only supports tokens of OAuth2Token type; Then exchange the incoming auth code for an access token; Then obtain user information (user name) according to the access token, and then create AuthenticationInfo according to this information; If you need authorization info information, you can obtain it according to the user name obtained here and your own business rules. It can be modified on the original OAuth2Realm class of the system.

Java code

@Setter
public class OAuth2Realm extends AuthorizingRealm {  
    private String clientId;  
    private String clientSecret;  
    private String accessTokenUrl;  
    private String userInfoUrl;  
    private String redirectUrl;  
    //Omit setter  
    public boolean supports(AuthenticationToken token) {  
        return token instanceof OAuth2Token; //Indicates that this Realm only supports OAuth2Token type  
    }  
    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(); //Get auth code  
        String username = extractUsername(code); // Extract user name  
        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();  
            //Get access token  
            OAuthAccessTokenResponse oAuthResponse =   
                oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);  
            String accessToken = oAuthResponse.getAccessToken();  
            Long expiresIn = oAuthResponse.getExpiresIn();  
            //Get 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);  
        }  
    }  
}  

The modified code (modified doGetAuthenticationInfo method) is implemented on the original OAuth2Realm class of the system. Can refer to

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(); //Get auth code
        final String loginName = extractUsername(code); // Extract user name

        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())) {
            // Prevent modification of system administrator properties in the database
            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 configuration (spring-config-shiro.xml)

Java code

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

The Spring shiro configuration modified on the oaut2realm class of the original system is for reference only
http://localhost:9080/chapter17 -Client / oauth2 login is the address of the target downstream system after successful login
Java code

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

If you are creating a new oaut2realm class, pay attention to the configuration of the securityManager

Configure oaut2authenticationfilter
Java code

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

This OAuth2AuthenticationFilter is used to intercept auth code s redirected back by the server.

Java code

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

Set loginUrl to 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 "; it will be automatically set to all accesscontrolfilters, such as oaut2authenticationfilter; in addition, / oauth2 login = oauth2authcmeans that the / oauth2 login address is intercepted by the oauth2autc interceptor and authorized by the oauth2 client.

So far, all configurations are completed

proposal

It is better not to use the url after the successful login of the target downstream system as the callback address, because it will be the same as the url of the system jumping to the home page after the filter verification is successful, resulting in the filter verification again. When the filter is verified again, because the system jumps to the home page, the filter verification will fail and return to the unified portal interface.

The solution is to create a special url for integrated login to be used as filter identification. After successful identification, jump to the home page to avoid re verification of the filter.

give an example:
The url of the original system after successful login is: http://192.168.11.54:8080/main
Set to create a special URL for integrated login: http://192.168.11.54:8080/userAuth/oAuth2 ;

@RestController
@RequestMapping(SystemConsts.UserAuth.CONTROLLER)
public class UserAuthController extends BaseCswController {
     //Other codes are omitted
       
     //Set to create a dedicated url for the integrated login
     @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;
    }
}

Fine tune the oauthauthenticationfilter configuration

<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   //Where to make changes
                /static/** = anon
                /** = authc
            </value>
        </property>
    </bean>

Original reference: https://blog.csdn.net/qq_32347977/article/details/51093895

Tags: Java Shiro Spring

Posted on Wed, 13 Oct 2021 13:18:50 -0400 by kee2ka4