1.概述

在本教程中,我们将使用OAuth保护REST API并从简单的Angular客户端使用它

我们要构建的应用程序将包含四个独立的模块:
  • 授权服务器
  • 资源服务器
  • UI implicit - 使用implicit流的前端应用程序
  • UI密码 - 使用密码流的前端应用程序

在我们开始之前 -** 一个重要的注意事项。请记住,Spring Security核心团队正在实施新的OAuth2堆栈 - 某些方面已经完成,有些方面仍在进行中**。

这是一个快速视频,将为您提供有关该工作的一些背景信息

https://youtu.be/YI4YCJoOF0k

2.授权服务器

首先,让我们开始将Authorization Server设置为一个简单的Spring Boot应用程序。

2.1。 Maven配置

我们将设置以下依赖项集:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>

请注意,我们使用的是spring-jdbc和MySQL,因为我们将使用JDBC支持的令牌存储实现

2.2。 @EnableAuthorizationServer

现在,让我们开始配置负责管理访问令牌的授权服务器:

@Configuration
@EnableAuthorizationServer
public class AuthServerOAuth2Config
extends AuthorizationServerConfigurerAdapter { @Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager; @Override
public void configure(
AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
} @Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.jdbc(dataSource())
.withClient("sampleClientId")
.authorizedGrantTypes("implicit")
.scopes("read")
.autoApprove(true)
.and()
.withClient("clientIdPassword")
.secret("secret")
.authorizedGrantTypes(
"password","authorization_code", "refresh_token")
.scopes("read");
} @Override
public void configure(
AuthorizationServerEndpointsConfigurer endpoints)
throws Exception { endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
} @Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
}

注意:

  • 为了持久化令牌,我们使用了JdbcTokenStore
  • 我们为“implicit”授权类型注册了客户
  • 我们注册了另一个客户端并授权了“password”,“authorization_code”和“refresh_token”授权类型
  • 为了使用“密码”授权类型,我们需要连接并使用AuthenticationManager bean

2.3。数据源配置

接下来,让我们配置JdbcTokenStore使用的数据源:

@Value("classpath:schema.sql")
private Resource schemaScript; @Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(databasePopulator());
return initializer;
} private DatabasePopulator databasePopulator() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(schemaScript);
return populator;
} @Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}

请注意,由于我们使用JdbcTokenStore,我们需要初始化数据库模式,因此我们使用了DataSourceInitializer - 以及以下SQL模式:

drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(255)
); drop table if exists oauth_client_token;
create table oauth_client_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255)
); drop table if exists oauth_access_token;
create table oauth_access_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONG VARBINARY,
refresh_token VARCHAR(255)
); drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
token_id VARCHAR(255),
token LONG VARBINARY,
authentication LONG VARBINARY
); drop table if exists oauth_code;
create table oauth_code (
code VARCHAR(255), authentication LONG VARBINARY
); drop table if exists oauth_approvals;
create table oauth_approvals (
userId VARCHAR(255),
clientId VARCHAR(255),
scope VARCHAR(255),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
); drop table if exists ClientDetails;
create table ClientDetails (
appId VARCHAR(255) PRIMARY KEY,
resourceIds VARCHAR(255),
appSecret VARCHAR(255),
scope VARCHAR(255),
grantTypes VARCHAR(255),
redirectUrl VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(255)
);

请注意,我们不一定需要显式的DatabasePopulator bean - 我们可以简单地使用schema.sql - Spring Boot默认使用它

2.4。Security配置

最后,让我们保护授权服务器。

当客户端应用程序需要获取访问令牌时,它将在简单的表单登录驱动的身份验证过程之后执行

@Configuration
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.withUser("john").password("123").roles("USER");
} @Override
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
} @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}

这里的一个简单说明是密码流不需要表单登录配置 - 仅适用于隐式流 - 因此您可以根据您正在使用的OAuth2流来跳过它。

3.资源服务器

现在,让我们讨论资源服务器;这本质上是我们最终希望能够使用的REST API。

3.1。 Maven配置

我们的资源服务器配置与先前的授权服务器应用程序配置相同。

3.2。令牌存储配置

接下来,我们将配置TokenStore以访问授权服务器用于存储访问令牌的同一数据库:

@Autowired
private Environment env; @Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
} @Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}

请注意,对于这个简单的实现,我们共享SQL支持的令牌存储,即使授权和资源服务器是单独的应用程序

当然,原因是资源服务器需要能够检查授权服务器发出的访问令牌的有效性。

3.3。远程令牌服务

我们可以使用RemoteTokeServices而不是在Resource Server中使用TokenStore:

@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(
"http://localhost:8080/spring-security-oauth-server/oauth/check_token");
tokenService.setClientId("fooClientIdPassword");
tokenService.setClientSecret("secret");
return tokenService;
}
注意:
  • 此RemoteTokenService将使用授权服务器上的CheckTokenEndPoint来验证AccessToken并从中获取Authentication对象。
  • 可以在AuthorizationServerBaseURL +“/ oauth / check_token”找到
  • Authorization Server可以使用任何TokenStore类型[JdbcTokenStore,JwtTokenStore,...] - 这不会影响RemoteTokenService或Resource服务器。

3.4。 Controller样例

接下来,让我们实现一个公开Foo资源的简单控制器:

@Controller
public class FooController { @PreAuthorize("#oauth2.hasScope('read')")
@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
@ResponseBody
public Foo findById(@PathVariable long id) {
return
new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
}
}

请注意客户端如何需要“read” scope来访问此资源。

我们还需要启用全局方法安全性并配置MethodSecurityExpressionHandler

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig
extends GlobalMethodSecurityConfiguration { @Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}

这是我们的基本Foo资源

public class Foo {
private long id;
private String name;
}

3.5。 Web配置

最后,让我们为API设置一个非常基本的Web配置:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig implements WebMvcConfigurer {}

4.前端 - 设置

我们现在将查看客户端的简单前端Angular实现。

首先,我们将使用Angular CLI生成和管理我们的前端模块

首先,我们将安装node和npm - 因为Angular CLI是一个npm工具

然后,我们需要使用frontend-maven-plugin使用maven构建我们的Angular项目

<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.3</version>
<configuration>
<nodeVersion>v6.10.2</nodeVersion>
<npmVersion>3.10.10</npmVersion>
<workingDirectory>src/main/resources</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

最后,使用Angular CLI生成一个新模块:

ng new oauthApp

请注意,我们将有两个前端模块 - 一个用于密码流,另一个用于隐式流

在以下部分中,我们将讨论每个模块的Angular app逻辑

5.使用Angular的密码流

我们将在这里使用OAuth2密码流 - 这就是为什么这只是一个概念证明,而不是生产就绪的应用程序。您会注意到客户端凭据已暴露给前端 - 这是我们将在以后的文章中介绍的内容。

我们的用例很简单:一旦用户提供其凭据,前端客户端就会使用它们从授权服务器获取访问令牌

5.1。应用服务

让我们从位于app.service.ts的AppService开始 - 它包含服务器交互的逻辑:

  • obtainAccessToken():获取给定用户凭据的Access令牌
  • saveToken():使用ng2-cookies库将访问令牌保存在cookie中
  • getResource():使用其ID从服务器获取Foo对象
  • checkCredentials():检查用户是否已登录
  • logout():删除访问令牌cookie并将用户注销
export class Foo {
  constructor(
    public id: number,
    public name: string) { }
}
 
@Injectable()
export class AppService {
  constructor(
    private _router: Router, private _http: Http){}
  
  obtainAccessToken(loginData){
    let params = new URLSearchParams();
    params.append('username',loginData.username);
    params.append('password',loginData.password);   
    params.append('grant_type','password');
    params.append('client_id','fooClientIdPassword');
    let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")});
    let options = new RequestOptions({ headers: headers });
     
    this._http.post('http://localhost:8081/spring-security-oauth-server/oauth/token',
      params.toString(), options)
      .map(res => res.json())
      .subscribe(
        data => this.saveToken(data),
        err => alert('Invalid Credentials'));
  }
 
  saveToken(token){
    var expireDate = new Date().getTime() + (1000 * token.expires_in);
    Cookie.set("access_token", token.access_token, expireDate);
    this._router.navigate(['/']);
  }
 
  getResource(resourceUrl) : Observable<Foo>{
    var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Bearer '+Cookie.get('access_token')});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
                   .map((res:Response) => res.json())
                   .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }
 
  checkCredentials(){
    if (!Cookie.check('access_token')){
        this._router.navigate(['/login']);
    }
  }
 
  logout() {
    Cookie.delete('access_token');
    this._router.navigate(['/login']);
  }
}
注意:
  • 要获取访问令牌,我们将POST发送到“/ oauth / token”端点
  • 我们使用客户端凭据和Basic Auth来命中此端点
  • 然后,我们将发送用户凭据以及客户端ID和授予类型参数URL编码
  • 获取访问令牌后 - 我们将其存储在cookie中

cookie存储在这里特别重要,因为我们只是将cookie用于存储目的而不是直接驱动身份验证过程。这有助于防止跨站点请求伪造(CSRF)类型的攻击和漏洞。

5.2。登录组件

接下来,让我们看一下负责登录表单的LoginComponent:

@Component({
selector: 'login-form',
providers: [AppService],
template: `<h1>Login</h1>
<input type="text" [(ngModel)]="loginData.username" />
<input type="password" [(ngModel)]="loginData.password"/>
<button (click)="login()" type="submit">Login</button>`
})
export class LoginComponent {
public loginData = {username: "", password: ""}; constructor(private _service:AppService) {} login() {
this._service.obtainAccessToken(this.loginData);
}

5.3。主页组件

接下来,我们的HomeComponent负责显示和操作我们的主页:

@Component({
selector: 'home-header',
providers: [AppService],
template: `<span>Welcome !!</span>
<a (click)="logout()" href="#">Logout</a>
<foo-details></foo-details>`
}) export class HomeComponent {
constructor(
private _service:AppService){} ngOnInit(){
this._service.checkCredentials();
} logout() {
this._service.logout();
}
}

5.4。 Foo组件

最后,我们的FooComponent显示我们的Foo细节:

@Component({
selector: 'foo-details',
providers: [AppService],
template: `<h1>Foo Details</h1>
<label>ID</label> <span>{{foo.id}}</span>
<label>Name</label> <span>{{foo.name}}</span>
<button (click)="getFoo()" type="submit">New Foo</button>`
}) export class FooComponent {
public foo = new Foo(1,'sample foo');
private foosUrl = 'http://localhost:8082/spring-security-oauth-resource/foos/'; constructor(private _service:AppService) {} getFoo(){
this._service.getResource(this.foosUrl+this.foo.id)
.subscribe(
data => this.foo = data,
error => this.foo.name = 'Error');
}
}

5.5。应用组件

我们的简单AppComponent充当根组件:

@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
}) export class AppComponent {}

以及我们包装所有组件,服务和路由的AppModule:

@NgModule({
declarations: [
AppComponent,
HomeComponent,
LoginComponent,
FooComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent }])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

6.隐含flow

接下来,我们将重点关注Implicit Flow模块。

6.1。应用服务

同样,我们将从我们的服务开始,但这次我们将使用库angular-oauth2-oidc而不是自己获取访问令牌:

@Injectable()
export class AppService { constructor(
private _router: Router, private _http: Http, private oauthService: OAuthService){
this.oauthService.loginUrl = 'http://localhost:8081/spring-security-oauth-server/oauth/authorize';
this.oauthService.redirectUri = 'http://localhost:8086/';
this.oauthService.clientId = "sampleClientId";
this.oauthService.scope = "read write foo bar";
this.oauthService.setStorage(sessionStorage);
this.oauthService.tryLogin({});
} obtainAccessToken(){
this.oauthService.initImplicitFlow();
} getResource(resourceUrl) : Observable<Foo>{
var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
'Authorization': 'Bearer '+this.oauthService.getAccessToken()});
var options = new RequestOptions({ headers: headers });
return this._http.get(resourceUrl, options)
.map((res:Response) => res.json())
.catch((error:any) => Observable.throw(error.json().error || 'Server error'));
} isLoggedIn(){
if (this.oauthService.getAccessToken() === null){
return false;
}
return true;
} logout() {
this.oauthService.logOut();
location.reload();
}
}

请注意,在获取访问令牌后,每当我们从资源服务器中使用受保护资源时,我们都会通过Authorization标头使用它

6.2。主页组件

我们的HomeComponent处理我们简单的主页:

@Component({
selector: 'home-header',
providers: [AppService],
template: `
<button *ngIf="!isLoggedIn" (click)="login()" type="submit">Login</button>
<div *ngIf="isLoggedIn">
<span>Welcome !!</span>
<a (click)="logout()" href="#">Logout</a>
<br/>
<foo-details></foo-details>
</div>`
}) export class HomeComponent {
public isLoggedIn = false; constructor(
private _service:AppService){} ngOnInit(){
this.isLoggedIn = this._service.isLoggedIn();
} login() {
this._service.obtainAccessToken();
} logout() {
this._service.logout();
}
}

6.3。 Foo组件

我们的FooComponent与密码流模块完全相同。

6.4。应用模块

最后,我们的AppModule:

@NgModule({
declarations: [
AppComponent,
HomeComponent,
FooComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
OAuthModule.forRoot(),
RouterModule.forRoot([
{ path: '', component: HomeComponent }])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

7.运行前端

1.要运行我们的任何前端模块,我们需要首先构建应用程序:
mvn clean install
2.然后我们需要导航到我们的Angular app目录:
cd src/main/resources
3.最后,我们将启动我们的应用程序:
npm start

默认情况下,服务器将在端口4200上启动,以更改任何模块的端口更改

"start": "ng serve"

在package.json中使它在端口8086上运行,例如:

"start": "ng serve --port 8086"

8.结论

在本文中,我们学习了如何使用OAuth2授权我们的应用程序。

可以在GitHub项目中找到本教程的完整实现。

使用OAuth保护REST API并使用简单的Angular客户端的更多相关文章

  1. ASP.NET Web API与Owin OAuth:使用Access Toke调用受保护的API

    在前一篇博文中,我们使用OAuth的Client Credential Grant授权方式,在服务端通过CNBlogsAuthorizationServerProvider(Authorization ...

  2. Access Toke调用受保护的API

    ASP.NET Web API与Owin OAuth:使用Access Toke调用受保护的API 在前一篇博文中,我们使用OAuth的Client Credential Grant授权方式,在服务端 ...

  3. CI中如何保护RESTful API

    步骤5 保护RESTful API 为了保护RESTful API,可以在application/config/rest.php中设置安全保护级别,如下所示: $config['rest_auth'] ...

  4. [angularjs] MVC + Web API + AngularJs 搭建简单的 CURD 框架

    MVC + Web API + AngularJs 搭建简单的 CURD 框架 GitHub 地址:https://github.com/liqingwen2015/Wen.MvcSinglePage ...

  5. python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用

    python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用 redispy安装安装及简单使用:https://github.com/andymccurdy/r ...

  6. 阿里云api调用做简单的cmdb

    阿里云api调用做简单的cmdb 1 步骤 事实上就是调用阿里api.获取可用区,比方cn-hangzhou啊等等.然后在每一个区调用api 取ecs的状态信息,最好写到一个excel里面去.方便排序 ...

  7. C#中缓存的使用 ajax请求基于restFul的WebApi(post、get、delete、put) 让 .NET 更方便的导入导出 Excel .net core api +swagger(一个简单的入门demo 使用codefirst+mysql) C# 位运算详解 c# 交错数组 c# 数组协变 C# 添加Excel表单控件(Form Controls) C#串口通信程序

    C#中缓存的使用   缓存的概念及优缺点在这里就不多做介绍,主要介绍一下使用的方法. 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可:  <%@ Outp ...

  8. 微服务(入门四):identityServer的简单使用(客户端授权)

    IdentityServer简介(摘自Identity官网) IdentityServer是将符合规范的OpenID Connect和OAuth 2.0端点添加到任意ASP.NET核心应用程序的中间件 ...

  9. 运用socket实现简单的服务器客户端交互

    Socket解释: 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. Socket的英文原义是“孔”或“插座”.作为BSD UNIX的进程通信机制,取后一种意 ...

随机推荐

  1. 模拟jQuery的一些功能

    //getStyle function getStyle(obj,attr){ if(obj.currentStyle){ return obj.currentStyle[attr]; } else{ ...

  2. kubectl工具管理应用生命周期

    ######kubectl管理工具###### [root@k8s-master dashboard]# kubectl get pod NAME READY STATUS RESTARTS AGE ...

  3. redis cluster 实践总结

      最近项目接触到了redis cluster,现在趁着使用做一下总结,记录一下遇到过的问题,简单的概述一下常用到的命令和功能. 本篇文章主要是以运维的角度去讲述如何去更好的规划redis clust ...

  4. WPF ValidationRule 触发ErrorTemplate 的注意事项

    ValidationRule 验证时, 当验证失败后,再次验证成功, errorTemplate 还是触发, 不会被清掉. 因此需要主动调用 Validation.ClearInvalid(dtpTe ...

  5. 微信小程序 加载 HTML 标签

    肯定有小伙伴遇到过这个问题:加载的数据是一堆HTML 标签这就尴尬了,因为小程序没有提供 webview 来加载这些 HTML.但是不用慌,小程序不提供我们可以自己造个新轮子,自己造不出新轮子咱们找到 ...

  6. Scala学习——泛型[T]的6种使用(初)

    package com.dtspark.scala.basics /** * 1,scala的类和方法.函数都可以是泛型. * * 2,关于对类型边界的限定分为上边界和下边界(对类进行限制) * 上边 ...

  7. jmeter性能指标

    Aggregate Report 是 JMeter 常用的一个 Listener,中文被翻译为“聚合报告”.今天再次有同行问到这个报告中的各项数据表示什么意思,顺便在这里公布一下,以备大家查阅. 如果 ...

  8. 梯度算子(普通的+Robert + sobel + Laplace)

    1.水平垂直差分法: 2.Robert 算子梯度 3.sobel算子 4.拉普拉斯算子

  9. Silverlight 5 系列学习之二

    昨天学习了一下Silverlight基础感觉也没有什么特别之处,不过圈里朋友劝我不要深入学习了,因为ms已不再爱他的这个孩子了,好吧那就把上些简单的东西稍微过一下吧,要不然公司有什么需求要改的小弟不会 ...

  10. url&nbsp;传递参数(特殊字符)解决方法

    url 传递参数(特殊字符)解决方法 首先设置 apache 配置文件, server.xml 在 port=8080 那一行中加上 URIEcoding=GBK 有些符号在URL中是不能直接传递的, ...