Spring MVC实现Spring Security,Spring Stomp websocket Jetty嵌入式运行

使用Spring框架各个组件实现一个在线聊天网页,当有用户连接WebSocket,服务器监听到用户连接会使用Stomp推送最新用户列表,有用户断开刷新在线列表,实时推送用户聊天信息。引入Jetty服务器,直接嵌入整个工程可以脱离Java Web容器独立运行,使用插件打包成一个jar文件,就像Spring Boot一样运行,部署。

pom.xml 依赖

 <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <jetty.version>9.4.8.v20171121</jetty.version>
        <spring.version>5.0.4.RELEASE</spring.version>
        <jackson.version>2.9.4</jackson.version>
        <lombok.version>1.16.18</lombok.version>
        <dbh2.version>1.4.196</dbh2.version>
        <jcl.slf4j.version>1.7.25</jcl.slf4j.version>
        <spring.security.version>5.0.3.RELEASE</spring.security.version>
        <logback.version>1.2.3</logback.version>
        <activemq.version>5.15.0</activemq.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>${jetty.version}</version>
        </dependency>

        <!-- 添加websocket 依赖不然会出现 java.lang.IllegalStateException: No suitable default
            RequestUpgradeStrategy found -->
        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-server</artifactId>
            <version>${jetty.version}</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.websocket</groupId>
            <artifactId>websocket-api</artifactId>
            <version>${jetty.version}</version>
        </dependency>

        <!--spring mvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webflux</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!--spring security -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring.security.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring.security.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
            <version>${spring.security.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
            <version>${spring.security.version}</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-messaging</artifactId>
            <version>${spring.security.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${dbh2.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${jcl.slf4j.version}</version>
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-broker</artifactId>
            <version>${activemq.version}</version>
        </dependency>

        <dependency>
            <groupId>io.projectreactor.ipc</groupId>
            <artifactId>reactor-netty</artifactId>
            <version>0.7.2.RELEASE</version>
        </dependency>

    </dependencies>

1. 配置H2 嵌入式数据库

    @Bean  //内存模式
    public DataSource  dataSource(){
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase build = builder.setType(EmbeddedDatabaseType.H2)
                .addScript("db/sql/create-db.sql") //每次创建数据源都会执行脚本
                .addScript("db/sql/insert-data.sql")
                .build();
        return build;
    }

这种方式是利用Spring 内置的嵌入式数据库的数据源模板,创建的数据源,比较简单,但是这种方式不支持定制,数据只能保存在内存中,项目重启数据就会丢失了。

设置数据保存到硬盘

    @Bean
    public DataSource dataSource() {
            DriverManagerDataSource dataSource = new DriverManagerDataSource();
            dataSource.setDriverClassName("org.h2.Driver");
            dataSource.setUsername("embedded");
            dataSource.setPassword("embedded");
            dataSource.setUrl("jdbc:h2:file:./data;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;");
            return dataSource;
    }

如果你还想每次创建数据源执行初始化sql,使用org.springframework.jdbc.datasource.init.ResourceDatabasePopulator 装载sql 脚本用于初始化或清理数据库

    @Bean
    public ResourceDatabasePopulator databasePopulator() {
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        populator.addScript(schema);
        populator.addScripts(data);
        populator.setContinueOnError(true);
        return populator;
    }

设置DatabasePopulator 对象,用户数据源启动或者消耗的时候执行脚本

 @Bean
    public DataSourceInitializer initializer() {
        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDatabasePopulator(databasePopulator());
        initializer.setDataSource(dataSource());
        return initializer;
    }

启用H2 web Console

 @Bean(initMethod = "start",destroyMethod = "stop")
    public Server DatasourcesManager() throws SQLException {
        return Server.createWebServer("-web","-webAllowOthers","-webPort","8082");
    }

浏览器打开 http://localhost:8082 访问H2 控制台

设置事务管理器

 @Bean
    public PlatformTransactionManager transactionManager() {
        PlatformTransactionManager manager = new DataSourceTransactionManager(dataSource());
        return manager;
    }
}

到这里,嵌入H2数据库配置基本已经设置完成了

2. Spring MVC配置

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.ting.spring.controller", //基包路径设置
        includeFilters = @ComponentScan.Filter(value = 
                {ControllerAdvice.class,Controller.class})) //只扫描MVC controll的注解
public class WebMvcConfiguration implements WebMvcConfigurer {


    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter());
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //添加静态路径映射
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

3. Jetty嵌入式服务

因为Spring 注解扫描只能注册一个类, 使用@Import引入其他的配置类

@Configuration
@ComponentScan(basePackages = "org.ting.spring",
        excludeFilters = {@ComponentScan.Filter(value = {Controller.class,ControllerAdvice.class})})
@Import({WebMvcConfiguration.class}) //引入Spring MVC配置类
public class WebRootConfiguration {

    @Autowired
    private DataSource dataSource;

    @Bean
    public JdbcTemplate jdbcTemplate(){
        JdbcTemplate template = new JdbcTemplate(dataSource);
        return template;
    }
}

使用Spring AnnotationConfigWebApplicationContext启动注解扫描,注册创建bean将WebApplicationContext,在将对象传给DispatcherServlet

public class JettyEmbedServer {

    private final static int DEFAULT_PORT = 9999;

    private final static String DEFAULT_CONTEXT_PATH = "/";

    private final static String MAPPING_URL = "/*";

    public static void main(String[] args) throws Exception {
        Server server = new Server(DEFAULT_PORT);
        JettyEmbedServer helloServer = new JettyEmbedServer();
        server.setHandler(helloServer.servletContextHandler());
        server.start();
        server.join();

    }

    private ServletContextHandler servletContextHandler() {
        WebApplicationContext context = webApplicationContext();
        ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
        servletContextHandler.setContextPath(DEFAULT_CONTEXT_PATH);
        ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(context));
        servletHolder.setAsyncSupported(true);
        servletContextHandler.addServlet(servletHolder, MAPPING_URL);
        return servletContextHandler;
    }

    private WebApplicationContext webApplicationContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(WebRootConfiguration.class);
        return context;
    }                  

3. 配置Spring Security

默认Spring Security拦截请求,登录失败,登录成功都是页面跳转的方式,我们希望ajax请求的时候,无论是被拦截了,或者登录失败,成功都可以返回json格式数据,由前端人员来处理。
根据HttpRequestServlet 请求头 X-Requested-With是否等于XMLHttpRequest 判断是否是ajax。

public class RespnonseJson {

    public static void jsonType(HttpServletResponse response) {
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
    }

    public static boolean ajaxRequest(HttpServletRequest request){
        String header = request.getHeader("X-Requested-With");
        return ! StringUtils.isEmpty(header) && header.equals("XMLHttpRequest");
    }

    public static boolean matchURL(String url) {
        Pattern compile = Pattern.compile("^/api/.+");
        return compile.matcher(url).matches();
    }
}

登录认证处理器

public class RestAuthenticationEntryPoint  extends LoginUrlAuthenticationEntryPoint {

    /**
     * @param loginFormUrl URL where the login page can be found. Should either be
     *                     relative to the web-app context path (include a leading {@code /}) or an absolute
     *                     URL.
     */
    public RestAuthenticationEntryPoint(String loginFormUrl) {
        super(loginFormUrl);
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String uri = request.getRequestURI();
        if (matchURL(uri)) {  //  /api 都是ajax 请求
            jsonType(response);
            response.getWriter().println(getErr(authException.getMessage()));
        }else if (ajaxRequest(request)){
            jsonType(response);
            response.getWriter().println(getErr(authException.getMessage()));
        }else super.commence(request,response,authException);
    }

    private String getErr(String description) throws JsonProcessingException {
        Result result = Result.error(Result.HTTP_FORBIDDEN, description);
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(result);
    }
}

登录成功处理

public class RestAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String uri = request.getRequestURI();
        if (matchURL(uri)){
            jsonType(response);
            String value = loginSuccess();
            response.getWriter().println(value);
        }else if (ajaxRequest(request)){
            jsonType(response);
            String success = loginSuccess();
            response.getWriter().println(success);
        }else super.onAuthenticationSuccess(request,response,authentication);
    }

    private String loginSuccess() throws JsonProcessingException {
        Result success = Result.success("sign on success go to next!");
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(success);
    }
}

登录失败处理

public class RestAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        if (ajaxRequest(request)){
            jsonType(response);
            String err = getErr(exception.getMessage());
            response.getWriter().println(err);
        }else super.onAuthenticationFailure(request,response,exception);
    }

    public String getErr(String description) throws JsonProcessingException {
        Result result = Result.error(Result.HTTP_AUTH_FAILURE, description);
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(result);
    }
}

我在网上搜索ajax 认证错误,很多博客是这样写的

response.sendError(500, "Authentication failed");

这个错误会被Jetty 错误页面捕获,扰乱返回JSON数据,这个细节要注意下

注册Handler

   @Bean
    public AuthenticationEntryPoint entryPoint() {
        RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint("/static/html/login.html"); 
        return entryPoint;
    }

    @Bean
    public SimpleUrlAuthenticationSuccessHandler successHandler() {
        RestAuthSuccessHandler successHandler = new RestAuthSuccessHandler();
        return successHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler failureHandler() {
        RestAuthFailureHandler failureHandler = new RestAuthFailureHandler();
        return failureHandler;
    }

配置url 认证

    @Bean
    public SessionRegistry sessionManager() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(entryPoint())
                .and()
                .authorizeRequests()
                .antMatchers("/static/html/jetty-chat.html",
                        "/api/user/online", "/api/user/loginuser") 
                .authenticated() //设置需要认证才可以请求的接口
                .and()
                .formLogin()
                .successHandler(successHandler()) //登录成功处理
                .failureHandler(failureHandler()) //登录失败处理
                .loginPage("/static/html/login.html") //登录页面
                .loginProcessingUrl("/auth/login") //登录表单url 
                .defaultSuccessUrl("/static/html/jetty-chat.html") //成功跳转url
                .permitAll()
                .and().csrf().disable()//禁用csrf 因为没有使用模板引擎
                .sessionManagement().maximumSessions(1)  //设置同一个账户,同时在线次数
                .sessionRegistry(sessionManager()) // 设置Session 管理器,
                .expiredUrl("/static/html/login.html") //session 失效后,跳转url
                .maxSessionsPreventsLogin(false) //设置true,达到session 最大登录次数后,后面的账户都会登录失败,false 顶号 前面登录账户会被后面顶下线
        ;

      //注销账户,跳转到登录页面
     http.logout().logoutUrl("/logout").logoutSuccessUrl("/static/html/login.html");

在配置类添加@EnableWebSecurity,在扫描类上引入Spring Security配置,大功告成了,并没有!Spring Security 是使用Filter来处理一些认证请求,需要我们在Jetty中手动注册拦截器

        //手动注册拦截器,让Spring Security 生效
        FilterHolder filterHolder = new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain"));
        servletContextHandler.addFilter(filterHolder, MAPPING_URL, null);
        servletContextHandler.addEventListener(new ContextLoaderListener(context));
        servletContextHandler.addEventListener(new HttpSessionEventPublisher()); //使用security session 监听器 限制只允许一个用户登录

4. 配置WebSocketStompConfig

@Configuration
@EnableWebSocketMessageBroker
@ComponentScan(basePackages = "org.ting.spring.stomp.message")
@Slf4j
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    //设置连接的端点路径
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("endpoint").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
        registry.enableSimpleBroker("/topic", "/queue");
        // 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
        registry.setApplicationDestinationPrefixes("/app");
        //使用客户端一对一通信
        registry.setUserDestinationPrefix("/user");
        registry.setPathMatcher(new AntPathMatcher("."));
    }

}

配置stomp 频道认证

@Configuration
public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
       messages.simpDestMatchers("/user/**").authenticated()//认证所有user 链接
       .anyMessage().permitAll();
    }

    //允许跨域  不然会出现 Could not verify the provided CSRF token because your session was not found 异常
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

信息处理

@Controller
@Slf4j
public class StompController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;


    @MessageExceptionHandler
    @SendToUser("/queue.errors")
    public String handleException(Throwable exception) {
        return exception.getMessage();
    }

    @MessageMapping("receive.messgae")
    public void forwardMsg(ChatMessage message){
        log.info("message :  {}",message);
        message.setLocalDateTime(LocalDateTime.now());
        messagingTemplate.convertAndSendToUser(message.getTargetUser().getEmail()
                ,"queue.notification",message);
    }

}

@MessageMapping 作用与@RequestMapping 功能差不多用于匹配url
更多Spring WebSocket 官方文档查看

我们使用一个集合来保存连接上的用户,使用连接,断开监听器来修改集合的列表,并将集合的数据发布到频道上。

websocket 断开连接监听器

@Component
@Slf4j
public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> {

    @Autowired
    private UserService userService;

    @Autowired
    private SimpMessagingTemplate messageTemplate;

    @Override
    public void onApplicationEvent(SessionDisconnectEvent event) {
        Principal principal = event.getUser();
        log.info("client sessionId : {} name : {} disconnect ....",event.getSessionId(),principal.getName());
        if (principal != null){ //已经认证过的用户
            User user = userService.findByEmail(principal.getName());
            Online.remove(user);
            messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers());
        }
    }
}

注册连接websocket 监听器

@Component
@Slf4j
public class WebSocketSessionConnectEvent implements ApplicationListener<SessionConnectEvent>{

    @Autowired
    private SimpMessagingTemplate messageTemplate;

    @Autowired
    private UserService userService;

    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        Principal principal = event.getUser();
        log.info("client name: {} connect.....",principal.getName());
        if (principal != null){
            User user = userService.findByEmail(principal.getName());
            Online.add(user);
            messageTemplate.convertAndSend("/topic/user.list",Online.onlineUsers());
        }
    }
}

保存在线列表

public class Online {

    private static Map<String,User> maps = new ConcurrentHashMap<>();

    public static void add(User user){
        maps.put(user.getEmail(),user);
    }

    public static void remove(User user){
        maps.remove(user.getEmail());
    }

    public static Collection<User> onlineUsers(){
        return maps.values();
    }

}

4. Spring Security OAuth2 Client 配置

手动配置ClientRegistrationRepository 设置client-id,client-secret,redirect-uri-template

   @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(githubClientRegstrationRepository()
                ,googleClientRegistrionRepository());
    }

    public ClientRegistration githubClientRegstrationRepository(){
        return CommonOAuth2Provider.GITHUB.getBuilder("github")
                .clientId(env.getProperty("registration.github.client-id"))
                .clientSecret(env.getProperty("registration.github.client-secret"))
                .redirectUriTemplate(env.getProperty("registration.github.redirect-uri-template"))
                .build();
    }

    public ClientRegistration googleClientRegistrionRepository(){
         return CommonOAuth2Provider.GOOGLE.getBuilder("google")
                .clientId(env.getProperty("registration.google.client-id"))
                .clientSecret(env.getProperty("registration.google.client-secret"))
                .redirectUriTemplate(env.getProperty("registration.google.redirect-uri-template"))
                .scope( "profile", "email")
                .build();
    }

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService() {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());
    }

我们使用github,google OAuth2 授权登录的账户,登录通过后保存起来,则需求继承DefaultOAuth2UserService


@Service
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserService userService;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        try {
           oAuth2User = processOAuth2User(oAuth2User,userRequest);
        } catch (Exception e) {
            log.error("processOAuth2User error {}",e);
        }
        return oAuth2User;
    }

    private OAuth2User processOAuth2User(OAuth2User oAuth2User,OAuth2UserRequest userRequest) {
        String clientId = userRequest.getClientRegistration().getRegistrationId();
        if (clientId.equalsIgnoreCase("github")) {
            Map<String, Object> map = oAuth2User.getAttributes();
            String login =  map.get("login")+"_oauth_github";
            String name = (String) map.get("name");
            String avatarUrl = (String) map.get("avatar_url");
            User user = userService.findByEmail(login);
            if (user == null) {
                user = new User();
                user.setUsername(name);
                user.setEmail(login);
                user.setAvatar(avatarUrl);
                user.setPassword("123456");
                userService.insert(user);
            }else {
                user.setUsername(name);
                user.setAvatar(avatarUrl);
                userService.update(user);
            }
            return UserPrincipal.create(user, oAuth2User.getAttributes());
        }else if (clientId.equalsIgnoreCase("google")){
            Map<String, Object> result = oAuth2User.getAttributes();
            String email = result.get("email")+"_oauth_google";
            String username = (String) result.get("name");
            String imgUrl = (String) result.get("picture");
            User user = userService.findByEmail(email);
            if (user == null){
                user = new User();
                user.setEmail(email);
                user.setPassword("123456");
                user.setAvatar(imgUrl);
                user.setUsername(username);
                userService.insert(user);
            }else {
                user.setUsername(username);
                user.setAvatar(imgUrl);
                userService.update(user);
            }
            return UserPrincipal.create(user,oAuth2User.getAttributes());
        }
        return null;
    }
}
 

重写UserDetails

public class UserPrincipal implements OAuth2User,UserDetails {

    private long id;

    private String name;

    private String password;

    private boolean enable;

    private Collection<? extends GrantedAuthority> authorities;

    private Map<String,Object> attributes;

    UserPrincipal(long id,String name,String password,boolean enable,Collection<? extends GrantedAuthority> authorities){
        this.id = id;
        this.name = name;
        this.password = password;
        this.authorities = authorities;
        this.enable = enable;
    }

    public static UserPrincipal create(User user){
        return new UserPrincipal(user.getId(),user.getEmail()
                ,user.getPassword(),user.isEnable(),Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
    }

    public static UserPrincipal create(User user, Map<String, Object> attributes) {
        UserPrincipal userPrincipal = UserPrincipal.create(user);
        userPrincipal.attributes = attributes;
        return userPrincipal;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return name;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.enable;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return this.attributes;
    }

    @Override
    public String getName() {
        return String.valueOf(this.id);
    }
}

设置Spring Security OAuth2 Client

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login()
                .clientRegistrationRepository(clientRegistrationRepository())
                .authorizedClientService(authorizedClientService())
                .userInfoEndpoint()
                .userService(customOAuth2UserService)
                .and()
            .defaultSuccessUrl("/static/html/jetty-chat.html");
}
}

默认授权端点,点击后直接重定向到授权服务器的登录页面,Spring 默认是:oauth2/authorization/{clientId}
默认授权成功跳转url/login/oauth2/code/{clientId}

这个项目参考的教程:
https://www.baeldung.com/spring-security-5-oauth2-login
https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/

这个教程只展示了一部分的代码,想查看完整的项目代码,可以去github: spring-stomp-security-webflux-embedded-jetty查看

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容