SpringMVC + Shiro integrate oauth2

SpringMVC + Shiro integrate oauth2

About client implementation (target downstream system) and error-prone problem analysis

At present, many open platforms such as the Sina Weibo open platform are using open API interfaces for developers to use, which brings about the problem that third-party applications need to go to the open platform for authorization. OAuth is for this, and OAuth2 is the OAuth protocol. In the next version, compared with OAuth1, the entire authorization process of OAuth2 is simpler and more secure, but it is not compatible with OAuth1. For details, you can go to the official website of OAuth2 at http://oauth.net/2/. OAuth2 protocol specifications can refer to http://tools. ietf.org/html/rfc6749. There are currently many reference implementations to choose from, which can be viewed and downloaded from their official website.

This article uses Apache Oltu, whose previous name was Apache Amber, which is the reference implementation of the Java version. For usage documentation, please refer to 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 an end user; such as Sina Weibo user zhangsan; resource server: stores protected resources
, The client requests resources through the access token, and the resource server responds with protected resources to the client; it stores information such as user zhangsan's Weibo.
Authorization server: After successfully verifying the resource owner and obtaining authorization, the authorization server issues an authorization token (Access Token) to the client.
Client: third-party applications such as Sina Weibo client weico and WeiGe, or its own official application; it does not store resources itself, but uses its authorization after the resource owner has authorized it ( Authorization token) to access protected resources, and then the client displays/submits the corresponding data to the server. The term "client" does not refer to any specific implementation (such as the application running on a server, desktop, mobile phone or other device).

OAuth2 protocol process

insert image description here

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

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

About client implementation

Client process: If you need to log in, first jump to the oauth2 server for login authorization. After success, the server returns the auth code, and then the client uses the auth code to go to the server to exchange for an access token. It is best to obtain user information based on the access token to log in to the client Binding.

POM dependency

Here we use 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 Token

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

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

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

Code modified on 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;
    }
}

OAuth2AuthenticationFilter

The function of this filter is similar to the FormAuthenticationFilter used for authentication control on the oauth2 client; if the current user has not yet been authenticated, it will first determine whether there is a code in the URL (the auth code returned by the server), and if not, redirect to the server. Log in and authorize, and then return the auth code; then OAuth2AuthenticationFilter will use the auth code to create an OAuth2Token, and then submit it to Subject.login for login; then OAuth2Realm will perform corresponding login logic based on the OAuth2Token.
Java code

@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;  
    }  
}   

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

OAuth2Realm

This Realm first only supports OAuth2Token type Token; then exchanges the incoming auth code for access token; then obtains user information (user name) based on the access token, and then creates AuthenticationInfo based on this information; if AuthorizationInfo information is needed, you can use this The user name obtained here is then obtained according to its 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;  
    //省略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);  
        }  
    }  
}  

Code modified on the original OAuth2Realm class of the system (modified doGetAuthenticationInfo method). 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(); //获取 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)

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>   

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

    <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 oAuth2Realm class, pay attention to the configuration of securityManager
insert image description here
and configure oAuth2AuthenticationFilter
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 the auth code redirected back from 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 here 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 AccessControlFilter, such as oAuth2AuthenticationFilter; in addition, /oauth2-login = oauth2Authc means that the /oauth2-login address is intercepted by the oauth2Authc interceptor and authorized by the oauth2 client.

All configurations are now complete

suggestion

It is best 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 that the system jumps to the home page after the filter verification is successful, causing the filter to verify again, and when it is verified again, because The system jumps by itself, so it will cause the filter verification to fail and return to the unified portal interface.

The solution is: create a special URL for integrated login and use it for filter identification. After successful identification, jump to the home page to avoid filter verification again.

For example:
the url after successful login of the original system 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 {
     //其他代码省略
       
     //设置为集成登录创建一个专门的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;
    }
}

Fine-tuning the OAuth2AuthenticationFilter 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   //做修改的地方
                /static/** = anon
                /** = authc
            </value>
        </property>
    </bean>

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

Guess you like

Origin blog.csdn.net/qq_38696286/article/details/120749004