Spring Security开发安全的REST服务-学习笔记(5)

5.1 OAuth简介

OAuth协议简介对于2.同意授权部分,有以下几种授权模式
授权模式简介

5.2 SpringSocial简介

授权码模式简介
授权码模式
具体实现类介绍

具体实现类介绍

5.3-5.4 QQ登陆(借鉴代码有毒博主的代码)

注册成为QQ开发者并且本地测试的博客地址(传送门)
如果没有域名的话可以使用NAT来内网穿透

1.在新的版本中不存在SocialProperties

需要自己将需要的属性添加到自己的properties类中去

/**
 * @author lwj
 */
@Data
public class QQProperties {
    /**
     * Application id.
     */
    private String appId;

    /**
     * Application secret.
     */
    private String appSecret;

    private String providerId = "qq";
}

2.关于social的自动配置

== 由于在spring-boot-autoconfigure-2.0.4.RELEASE.jar没有对 social的自动配置了 ==

@Configuration
@ConditionalOnProperty(prefix = "moss.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(createConnectionFactory());
    }

    protected ConnectionFactory<?> createConnectionFactory() {
        QQProperties qqProperties = securityProperties.getSocial().getQq();
        return new QQConnectionFactory(qqProperties.getProviderId(), qqProperties.getAppId(), qqProperties.getAppSecret());
    }

    // 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
    // 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
    // 这里需要返回null,否则会返回内存的 ConnectionRepository
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return null;
    }
}

5.5 开发QQ登陆(下)

在截止上一节代码之后,点击QQ登录之后出现以下错误
衔接上一节课的错误截图

redirect url 即是在QQ互联平台注册的网站回调域
网站应用注册信息截图
请求链的图解

在这里插入图片描述在配置好后,点击QQ登录之后,在此页面上点击账号授权后,发现页面被重定向一个signin的路由,说明并未登录成功。
在这里插入图片描述
登录失败
在这里插入图片描述
页面回到需要登陆的路由下
在这里插入图片描述
此时发现是在拿到授权码之后交换令牌的时候出现了错误
在这里插入图片描述
从报错信息上可以看到这是应为无法处理[text/html]这种格式的响应,可以看到OAuth2Template中并不存在处理[text/html]这种类型的响应的converter
在这里插入图片描述
这里我们需要去写一个自定义的template来匹配这个返回结果的解析

/**
 * 自定义的template类
 *
 * @author lwj
 */
public class QQOAuth2Template extends OAuth2Template {

    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
    }

    /**
     * 添加处理【text/html】类型的响应的Converter,{@link StringHttpMessageConverter}
     * @return
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}

修改配置项中的template实例化对象
在这里插入图片描述
在获取令牌的方法中可以看到该方法将返回数据解析成一个map
在这里插入图片描述
然后在通过map来获取对应的值
在这里插入图片描述但是,查看文档后发现,返回值为字符串,所以我们需要自己去解析这个字符串,然后再调用
在这里插入图片描述
在QQOAuth2Template类中添加下列方法

@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
    String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
    log.info("获取accessToken的响应:" + responseStr);
    String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
    String accessToken = StringUtils.substringAfterLast(items[0], "=");
    Long expiresIn = Long.valueOf(StringUtils.substringAfterLast(items[1], "="));
    String refreshToken = StringUtils.substringAfterLast(items[2], "=");
    return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}

5.6 处理注册逻辑

截止到以上的代码已经能够出现QQ登陆的界面,但是在登陆后发现还是跳转到了需要登陆的界面。
在这里插入图片描述
在查看控制台打印的日志看到跳转的路由是signUp,
在这里插入图片描述该路由是在SocialAuthenticationFilter.class中定义,因为当SocialAuthenticationProvider中出现异常后,都会被该类收集并处理。

在这里插入图片描述
在查看SocialAuthenticationProvider中的代码可以发现,此处是由于去查询数据库中的moss_UserConnection表,但是目前并没有数据。userId为null时会抛出异常,被重定向到登陆的url中,如果项目中未添加注册页面则会报404错误。

5.6.1 添加注册功能

在SocialConfig类中添加如下配置

在这里插入图片描述
添加登陆工具类

@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
    return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator)) {
    };
}

BrowserProperties类中添加默认注册页面的路径

/** 默认注册页 */
private String signUpUrl = "/signUp.html";

在browser项目中添加signUp.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>
<body>
    <h2>标准注册界面</h2>
    <h3>这是系统注册页面,请配置</h3>
</body>
</html>

在demo项目中添加demo项目的注册页面demo-signUp

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
   <h2>Demo注册页</h2>
   <form action="/user/regist" method="post">
      <table>
         <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
         </tr>
         <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
         </tr>
         <tr>
            <td colspan="2">
               <button type="submit" name="type" value="regist">注册</button>
               <button type="submit" name="type" value="banding">绑定</button>
            </td>
         </tr>
      </table>
   </form>
</body>
</html>

在demo项目中的UserController中添加注册方法

@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
    // 不管是注册用户还是绑定用户,都会拿到用户地一个唯一标识(此处用用户的名称作为唯一标识)
    String userId = user.getUsername();
    providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}

5.6.2 添加自动注册功能

在这里插入图片描述
在上面的截图的空色框中的方法,我们可以发现当该方法中的 ConnectionSignUp类被实例化了之后,this.connectionSignUp.execute(connection)代码执行后会返回一个newUserId,并且下面的代码会将该id保存到数据库中,完成注册功能,那么我们只需要在项目中去实例化这个ConnectionSignUp类即可。
回到demo项目的security文件夹下新建DemoConnectionSignUp 类

package com.moss.securitydemo.security;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;

/**
 * 自动注册接口实现
 *  注释掉该@Component注解后需要通过注册页面来注册
 *  该类中判断了根据是否配置了ConnectionSignUp类来决定是否自动注册
 *  {@link org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository}
 *
 * @author lwj
 */
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
    /**
     * 复写该方法用来指定唯一标识
     * @param connection
     * @return
     */
    @Override
    public String execute(Connection<?> connection) {
        //  根据connection中的用户信息创建用户并返回唯一标识
        return connection.getDisplayName();
    }
}

5.7 开发微信登陆

微信登陆部分需要使用微信开放平台的开发者账号才可以做测试,暂不处理

5.8 绑定和解绑处理

5.8.1 提供绑定数据的接口(/connect)

该接口需要将ConnectController加入到Spring的bean中,需要去SocialConfig类中配置

@Bean
public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
    return new ConnectController(connectionFactoryLocator, connectionRepository);
}

查看ConnectController类的源码
在这里插入图片描述
执行后返回方法会返回一个视图,connect/status,那么就需要去配置一个视图来解析数据

@Component("connect/status")
public class MossConnectionStatusView extends AbstractView {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) map.get("connectionMap");

        Map<String, Boolean> result = new HashMap<>();
        for (String key : connections.keySet()) {
            result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
        }

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
    }

}

5.8.2 绑定功能开发

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>绑定</title>
</head>
<body>
    <h2>标准绑定界面</h2>
    <form action="/connect/qq" method="post">
        <button type="submit">绑定QQ</button>
    </form>
</body>
</html>

在绑定成功后会返回一个视图,所以需要去配置一个绑定成功的视图,同样的在解绑的时候,同样也会返回一个视图
在这里插入图片描述绑定成功:connect/qqConnected;解绑成功:connect/qqConnect
创建视图处理类

public class MossConnectView extends AbstractView {

    @Override
    protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        httpServletResponse.setContentType("text/html;charset=UTF-8");
        if (map.get("connection") == null) {
            httpServletResponse.getWriter().write("<h3>绑定成功!</h3>");
        } else {
            httpServletResponse.getWriter().write("<h3>解绑成功!</h3>");
        }
    }
}

将视图处理类添加到QQAutoConfig中

/**
 * 配置默认QQ绑定/解绑成功的返回页
 * @return
 */
@Bean({"connect/qqConnect", "connect/qqConnected"})
@ConditionalOnMissingBean(name = "qqConnectedView")
public View qqConnectedView() {
    return new MossConnectView();
}

当我们去添加测试绑定的时候会绑定的默认页面
发现,我们点击绑定了之后,页面出现如图错误,查看redirect uri发现是域名+/connect/qq,说明回调路由和当前的路由不匹配
在这里插入图片描述
我们需要去QQ互联平台上添加绑定的回调路由
在这里插入图片描述
至此,点击绑定后成功跳转到了QQ授权的页面,在点击了登陆授权后,页面跳转到
在这里插入图片描述

5.8.3 解绑功能开发

其实就是发送一个Method为DELETE的请求http://域名/connect/qq,同样也需要配置一个视图去解析,这个视图就是在上面的地方添加的

在这里插入图片描述
至此,绑定和解绑部分的功能就开发完毕了

5.9 单机Session管理

5.9.1 Session配置的工厂类

TomcatEmbeddedServletContainerFactory在Springboot 2.x中找不到,找到了另一个类TomcatServletWebServerFactory

5.9.1.1 设置小于1分钟的Session过期时间

设置了10秒的session超时时间在这里插入图片描述
但是发现过了10秒并没有过期,调查该类中的设置session的方法可以看到
在这里插入图片描述

5.9.1.2 设置session失效的默认跳转路径

在BrowserSecurityConfig类中的配置安全规则部分添加session部分的配置,并将session过期的路由添加到默认可以访问的路由中
在这里插入图片描述
在BrowserSecurityController中添加处理session过期的url

/**
 * 处理session过期
 * @return
 */
@GetMapping("/session/invalid")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public SimpleResponse sessionInvalid() {
    String message = "session失效";
    return new SimpleResponse(message);
}

5.9.1.3 设置单点登录

在上面的BrowserSecurityConfig中添加

.maximumSessions(1)
.expiredSessionStrategy(new MossExpiredSessionStrtegy())

在browser项目中创建session文件夹,并在文件夹下创建MossExpiredSessionStrtegy类

/**
 * session并发处理
 *
 * @author lwj
 */
public class MossExpiredSessionStrtegy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
        sessionInformationExpiredEvent.getResponse().setContentType("application/json;charset=utf-8");
        sessionInformationExpiredEvent.getResponse().getWriter().write("session并发!");
    }

}

在两个浏览器之间都登陆的情况下,第二个浏览器访问路由会报下面的情况说明配置完成
在这里插入图片描述
阻止第二个用户登陆
上面的配置在第二个用户登陆后,第一个用户继续访问会引发session并发,但是当如果我们需要阻止第二个用户登陆,来防止第一个用户的登陆被刷掉的话,那么我们需要在配置中增加一行配置

.maxSessionsPreventsLogin(true)

5.9.2 代码重构

重构部分的代码暂不在此处延伸。

5.10 Session集群管理

在这里插入图片描述

5.10.1 修改配置

在application.yml中添加

spring:
  session:
    store-type: redis

5.10.2 添加依赖

特别注意:spring-session:1.3.3.RELEASE在高版本的spring boot autoconfig中已经不支持了;
需要分开引用下面的包

<!--&lt;!&ndash; https://mvnrepository.com/artifact/org.springframework.session/spring-session &ndash;&gt;-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-core</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

5.10.3 启动发现验证码生成报错

修改验证码部分的代码

public class ImageCode extends ValidateCode implements Serializable {

    private static final long serialVersionUID = 8774439331692262265L;

修改AbstractValidateCodeProcessor类中将验证码保存到redis中的部分的代码

/**
 * 保存校验码
 *
 * @param servletWebRequest
 * @param validateCode
 */
private void save(ServletWebRequest servletWebRequest, C validateCode) {
    // 不保存图片对象到redis session中,无法序列化
    // 因为在验证的时候不需要图片对象
    ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
    sessionStrategy.setAttribute(servletWebRequest, getSessionKey(servletWebRequest), code);
}

5.11 退出登录

5.11.1 编写退出登录页面

在demo的Index.html中添加退出按钮

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    主页
    <a href="/signOut">退出登录</a>
</body>
</html>

启动项目后,测试发现点击后,跳转到了需要再次登录的链接上,这是因为Spring默认退出后跳转的是登录url
在这里插入图片描述

5.11.2 退出登录部分配置

图中的部分是配置了
logoutUrl:配置退出登录的url
logoutSuccessHandler:配置退出登录成功控制器
deleteCookies:配置退出登录成功后删除cookies
在这里插入图片描述

5.11.3 编写MossLogoutSuccessHandler实现LogoutSuccessHandler 接口

@Data
public class MossLogoutSuccessHandler implements LogoutSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    private String signOutUrl;

    public MossLogoutSuccessHandler(String signOutUrl) {
        this.signOutUrl = signOutUrl;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        logger.info("退出成功");

        if (StringUtils.isBlank(signOutUrl)) {
            httpServletResponse.setContentType("application/json;charset=UTF-8");
            httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
        } else {
            httpServletResponse.sendRedirect(signOutUrl);
        }
    }

}

在BrowserSecurityBeanConfig中配置MossLogoutSuccessHandler类

/**
 * 默认退出成功控制器
 * @return
 */
@Bean
@ConditionalOnMissingBean(LogoutSuccessHandler.class)
public LogoutSuccessHandler logoutSuccessHandler() {
   return new MossLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
}

将退出登录成功后跳转的页面配置到core项目中的BrowserProperties中
在这里插入图片描述
在demo项目中的application.yml文件中增加退出登录后跳转的页面的配置

signOutUrl: /demo-logout.html

在demo项目下创建demo-logout.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>退出成功</title>
</head>
<body>
    <h2>Demo退出成功</h2>
</body>
</html>

在BrowserSecurityConfig类中添加退出成功跳转页的url

securityProperties.getBrowser().getSignOutUrl()

猜你喜欢

转载自blog.csdn.net/weixin_38657051/article/details/89147083
今日推荐