javascript
Spring MVC实现Spring Security,Spring Stomp websocket Jetty嵌入式运行
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 defaultRequestUpgradeStrategy 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 內置的嵌入式數據庫的數據源模板,創建的數據源,比較簡單,但是這種方式不支持定制,數據只能保存在內存中,項目重啟數據就會丟失了。
設置數據保存到硬盤
@Beanpublic 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 腳本用于初始化或清理數據庫
@Beanpublic ResourceDatabasePopulator databasePopulator() {ResourceDatabasePopulator populator = new ResourceDatabasePopulator();populator.addScript(schema);populator.addScripts(data);populator.setContinueOnError(true);return populator;}設置DatabasePopulator 對象,用戶數據源啟動或者消耗的時候執行腳本
@Beanpublic 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 控制臺
設置事務管理器
@Beanpublic 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());}@Overridepublic 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 {@Autowiredprivate DataSource dataSource;@Beanpublic 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 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);}@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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
@Beanpublic AuthenticationEntryPoint entryPoint() {RestAuthenticationEntryPoint entryPoint = new RestAuthenticationEntryPoint("/static/html/login.html"); return entryPoint;}@Beanpublic SimpleUrlAuthenticationSuccessHandler successHandler() {RestAuthSuccessHandler successHandler = new RestAuthSuccessHandler();return successHandler;}@Beanpublic SimpleUrlAuthenticationFailureHandler failureHandler() {RestAuthFailureHandler failureHandler = new RestAuthFailureHandler();return failureHandler;}配置url 認證
@Beanpublic SessionRegistry sessionManager() {return new SessionRegistryImpl();}@Overrideprotected 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 {//設置連接的端點路徑@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("endpoint").withSockJS();}@Overridepublic 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 {@Overrideprotected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {messages.simpDestMatchers("/user/**").authenticated()//認證所有user 鏈接.anyMessage().permitAll();}//允許跨域 不然會出現 Could not verify the provided CSRF token because your session was not found 異常@Overrideprotected boolean sameOriginDisabled() {return true;} }信息處理
@Controller @Slf4j public class StompController {@Autowiredprivate 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> {@Autowiredprivate UserService userService;@Autowiredprivate SimpMessagingTemplate messageTemplate;@Overridepublic 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>{@Autowiredprivate SimpMessagingTemplate messageTemplate;@Autowiredprivate UserService userService;@Overridepublic 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
@Beanpublic 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();}@Beanpublic OAuth2AuthorizedClientService authorizedClientService() {return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());}我們使用github,google OAuth2 授權登錄的賬戶,登錄通過后保存起來,則需求繼承DefaultOAuth2UserService
@Service @Slf4j public class CustomOAuth2UserService extends DefaultOAuth2UserService {@Autowiredprivate UserService userService;@Overridepublic 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;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return name;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.enable;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic Map<String, Object> getAttributes() {return this.attributes;}@Overridepublic String getName() {return String.valueOf(this.id);} }設置Spring Security OAuth2 Client
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomOAuth2UserService customOAuth2UserService;@Overrideprotected 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查看
總結
以上是生活随笔為你收集整理的Spring MVC实现Spring Security,Spring Stomp websocket Jetty嵌入式运行的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql-存储过程
- 下一篇: Flask出现Error code 40