Mkyong-中文博客翻译-九-
Mkyong 中文博客翻译(九)
原文:Mkyong
从资源文件夹中读取文件
在 Spring 中,我们可以使用ClassPathResource
或ResourceLoader
轻松地从类路径中获取文件。
用弹簧 5.1.4 测试 P.S .释放
1.资源中心/主要/资源/
例如,src/main/resources/
文件夹中的图像文件
2.ClassPathResource
import org.springframework.core.io.Resource;
import org.springframework.core.io.ClassPathResource;
import java.io.File;
import java.io.InputStream;
Resource resource = new ClassPathResource("android.png");
InputStream input = resource.getInputStream();
File file = resource.getFile();
3.资源加载器
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.File;
import java.io.InputStream;
@Autowired
ResourceLoader resourceLoader;
Resource resource = resourceLoader.getResource("classpath:android.png");
InputStream input = resource.getInputStream();
File file = resource.getFile();
4.资源工具
请不要使用这个ResourceUtils
即使它有效,这个类主要是在框架内部使用。阅读 ResourceUtils JavaDocs
import org.springframework.util.ResourceUtils;
File file = ResourceUtils.getFile("classpath:android.png");
参考
带有 ResourceBundleMessageSource 示例的 Spring 资源包
在 Spring 中,您可以使用ResourceBundleMessageSource
来解析来自属性文件的文本消息,基于所选择的地区。请参见以下示例:
1.目录结构
查看此示例的目录结构。
2.属性文件
创建两个属性文件,一个用于英文字符(messages_en_US.properties
),另一个用于中文字符(messages_zh_CN.properties
)。将其放入项目类路径中(见上图)。
文件:messages_en_US.properties
customer.name=Yong Mook Kim, age : {0}, URL : {1}
文件:messages_zh_CN.properties
customer.name=\ufeff\u6768\u6728\u91d1, age : {0}, URL : {1}
'\ u eff \ u 6768 \ u 6728 \ u91d 1'为中文 Unicode 字符。
Note
To display the Chinese characters correctly, you have to use “native2ascii” tool to convert the Chinese characters into Unicode characters.
3.Bean 配置文件
将属性文件包含到 bean 配置文件中。“messages _ en _ us . properties”和“messages _ zh _ cn . properties”在 Spring 中都被认为是一个文件,你只需要包含文件名一次,Spring 就会自动找到正确的区域设置。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>locale\customer\messages</value>
</property>
</bean>
</beans>
假设这两个文件都位于“资源\区域\客户”文件夹。
4.运行它
package com.mkyong.common;
import java.util.Locale;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context
= new ClassPathXmlApplicationContext("locale.xml");
String name = context.getMessage("customer.name",
new Object[] { 28,"http://www.mkyong.com" }, Locale.US);
System.out.println("Customer name (English) : " + name);
String namechinese = context.getMessage("customer.name",
new Object[] {28, "http://www.mkyong.com" },
Locale.SIMPLIFIED_CHINESE);
System.out.println("Customer name (Chinese) : " + namechinese);
}
}
输出
Note
Make sure your Eclipse is able to display Chinese output.
说明
1.在context.getMessage()
中,第二个参数是消息参数,你必须将其作为对象数组传递。如果没有可用的参数值,可以只传递一个 null。
context.getMessage("customer.name",null, Locale.US);
2.地点。US 将从'messages _ en _ US . properties'中检索消息。简体中文将从“messages _ zh _ cn . properties中检索消息。
More …
Read this article to know how to access the MessageSource inside a bean.
下载源代码
Download it – Spring-MessageSource-Example.zipTags : resource bundle spring
带有 getResource()示例的 Spring 资源加载器
Spring 的资源加载器提供了一个非常通用的 getResource() 方法来从文件系统、类路径或 URL 获取资源,比如(文本文件、媒体文件、图像文件……)。您可以从应用程序上下文中获取 getResource() 方法。
下面的例子展示了如何使用 getResource() 从
1。文件系统
Resource resource = appContext.getResource("file:c:\\testing.txt");
2。URL 路径
Resource resource =
appContext.getResource("url:http://www.yourdomain.com/testing.txt");
3。类别路径
Resource resource =
appContext.getResource("classpath:com/mkyong/common/testing.txt");
您只需要指定资源位置,Spring 会处理剩下的工作并返回一个资源对象。
使用getResource()
方法的完整示例。
package com.mkyong.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
public class App
{
public static void main( String[] args )
{
ApplicationContext appContext =
new ClassPathXmlApplicationContext(new String[] {"If-you-have-any.xml"});
Resource resource =
appContext.getResource("classpath:com/mkyong/common/testing.txt");
try{
InputStream is = resource.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
Bean 资源加载器(ResourceLoaderAware)
既然 bean 没有应用程序上下文访问权限,那么 bean 如何访问资源呢?解决方法是实现resource loader ware接口,并为 ResourceLoader 对象创建 setter 方法。Spring 会将资源加载器插入到 bean 中。
package com.mkyong.customer.services;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
public class CustomerService implements ResourceLoaderAware
{
private ResourceLoader resourceLoader;
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public Resource getResource(String location){
return resourceLoader.getResource(location);
}
}
Bean 配置文件
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="customerService"
class="com.mkyong.customer.services.CustomerService" />
</beans>
运行它
package com.mkyong.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
import com.mkyong.customer.services.CustomerService;
public class App
{
public static void main( String[] args )
{
ApplicationContext appContext =
new ClassPathXmlApplicationContext(new String[] {"Spring-Customer.xml"});
CustomerService cust =
(CustomerService)appContext.getBean("customerService");
Resource resource =
cust.getResource("classpath:com/mkyong/common/testing.txt");
try{
InputStream is = resource.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
现在,您可以从 bean 中获取资源。
结论
如果没有这个 getResource()方法,您将需要用不同的解决方案处理不同的资源,比如文件系统资源的文件对象,URL 资源的 URL 对象。Spring 确实用这个超级通用的 getResource() 方法做得很好,它确实节省了我们处理资源的时间。
下载源代码
Download it – Spring-getResource-Example.zipspring
弹簧座错误处理示例
在本文中,我们将向您展示 Spring Boot REST 应用程序中的错误处理。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- maven3
- Java 8
1./错误
1.1 默认情况下,Spring Boot 为处理所有错误的/error
映射提供了一个BasicErrorController
控制器,并为生成包含错误细节、HTTP 状态和异常消息的 JSON 响应提供了一个getErrorAttributes
。
{
"timestamp":"2019-02-27T04:03:52.398+0000",
"status":500,
"error":"Internal Server Error",
"message":"...",
"path":"/path"
}
BasicErrorController.java
package org.springframework.boot.autoconfigure.web.servlet.error;
//...
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//...
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
在 IDE 中,在这个方法中放置一个断点,你将理解 Spring Boot 如何生成默认的 JSON 错误响应。
2.自定义异常
在 Spring Boot,我们可以使用@ControllerAdvice
来处理自定义异常。
2.1 自定义异常。
BookNotFoundException.java
package com.mkyong.error;
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book id not found : " + id);
}
}
如果没有找到图书 id,控制器抛出上述BookNotFoundException
BookController.java
package com.mkyong;
//...
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}
默认情况下,Spring Boot 生成以下 JSON 错误响应,http 500 error。
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27T04:03:52.398+0000",
"status":500,
"error":"Internal Server Error",
"message":"Book id not found : 5",
"path":"/books/5"
}
2.2 如果没有找到图书 id,它应该返回 404 错误而不是 500,我们可以像这样覆盖状态代码:
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Let Spring BasicErrorController handle the exception, we just override the status code
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
//...
}
2.3 它现在返回一个 404。
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27T04:21:17.740+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5"
}
2.4 此外,我们可以定制整个 JSON 错误响应:
CustomErrorResponse.java
package com.mkyong.error;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
public class CustomErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
private int status;
private String error;
//...getters setters
}
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<CustomErrorResponse> customHandleNotFound(Exception ex, WebRequest request) {
CustomErrorResponse errors = new CustomErrorResponse();
errors.setTimestamp(LocalDateTime.now());
errors.setError(ex.getMessage());
errors.setStatus(HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errors, HttpStatus.NOT_FOUND);
}
//...
}
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27 12:40:45",
"status":404,
"error":"Book id not found : 5"
}
3.JSR 303 验证错误
3.1 对于 Spring @valid
验证错误,会抛出handleMethodArgumentNotValid
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
// @Validate For Validating Path Variables and Request Parameters
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
// error handle for @Valid
@Override
protected ResponseEntity<Object>
handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
//Get all fields errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
4.ResponseEntityExceptionHandler
4.1 如果我们不确定 Spring Boot 抛出了什么异常,就在这个方法中放一个断点进行调试。
ResponseEntityExceptionHandler.java
package org.springframework.web.servlet.mvc.method.annotation;
//...
public abstract class ResponseEntityExceptionHandler {
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
}
//...
}
//...
}
5.DefaultErrorAttributes
5.1 为了覆盖所有异常的默认 JSON 错误响应,创建一个 bean 并扩展DefaultErrorAttributes
CustomErrorAttributes.java
package com.mkyong.error;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// Let Spring handle the error first, we will modify later :)
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
// format & update timestamp
Object timestamp = errorAttributes.get("timestamp");
if (timestamp == null) {
errorAttributes.put("timestamp", dateFormat.format(new Date()));
} else {
errorAttributes.put("timestamp", dateFormat.format((Date) timestamp));
}
// insert a new key
errorAttributes.put("version", "1.2");
return errorAttributes;
}
}
现在,日期时间被格式化,一个新字段–version
被添加到 JSON 错误响应中。
curl localhost:8080/books/5
{
"timestamp":"2019/02/27 13:34:24",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5",
"version":"1.2"
}
curl localhost:8080/abc
{
"timestamp":"2019/02/27 13:35:10",
"status":404,
"error":"Not Found",
"message":"No message available",
"path":"/abc",
"version":"1.2"
}
完成了。
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-error-handling
$ mvn spring-boot:run
参考
- Spring Boot 错误处理参考文献
- 默认错误属性文档
- ResponseEntityExceptionHandler
- 弹簧座验证示例
- Spring Boot 安全特征
- Hello Spring Security with Boot
- 维基百科–休息
春季休息你好世界示例
在本文中,我们将向您展示如何开发一个 Spring Boot REST 风格的 web 服务来处理来自 H2 内存数据库的 CRUD 操作。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- 春季数据 JPA 2.1.4.RELEASE
- H2 内存数据库 1.4.197
- Tomcat Embed 9.0.14
- JUnit 4.12
- maven3
- Java 8
1.项目目录
2.专家
包括用于 Spring MVC 和 REST 结构的spring-boot-starter-web
,用于 CRUD 库的spring-boot-starter-data-jpa
。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-rest-hello-world</artifactId>
<packaging>jar</packaging>
<name>Spring Boot REST API Example</name>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<!-- Java 8 -->
<properties>
<java.version>1.8</java.version>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
<dependencies>
<!-- spring mvc, rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jpa, crud repository -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- in-memory database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- unit test rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- test patch operation need this -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
<scope>test</scope>
</dependency>
<!-- hot swapping, disable cache for template, enable live reload -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
</plugins>
</build>
</project>
项目依赖关系。
$ mvn dependency:tree
[INFO] org.springframework.boot:spring-rest-hello-world:jar:1.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.2.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.1:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.1:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.23:runtime
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.8:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.8:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.14:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.14:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.14:compile
[INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.14.Final:compile
[INFO] | | +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | \- com.fasterxml:classmate:jar:1.4.0:compile
[INFO] | +- org.springframework:spring-web:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-webmvc:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-aop:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-context:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-expression:jar:5.1.4.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.1.2.RELEASE:compile
[INFO] | | \- org.aspectj:aspectjweaver:jar:1.9.2:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:3.2.0:compile
[INFO] | | \- org.springframework:spring-jdbc:jar:5.1.4.RELEASE:compile
[INFO] | +- javax.transaction:javax.transaction-api:jar:1.3:compile
[INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile
[INFO] | | \- javax.activation:javax.activation-api:jar:1.2.0:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.3.7.Final:compile
[INFO] | | +- javax.persistence:javax.persistence-api:jar:2.2:compile
[INFO] | | +- org.javassist:javassist:jar:3.23.1-GA:compile
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.9.7:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.0.5.Final:compile
[INFO] | | +- org.dom4j:dom4j:jar:2.1.1:compile
[INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.4.Final:compile
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-tx:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | \- org.springframework:spring-aspects:jar:5.1.4.RELEASE:compile
[INFO] +- com.h2database:h2:jar:1.4.197:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.1.2.RELEASE:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | | \- net.minidev:json-smart:jar:2.3:test
[INFO] | | \- net.minidev:accessors-smart:jar:1.2:test
[INFO] | | \- org.ow2.asm:asm:jar:5.0.4:test
[INFO] | +- junit:junit:jar:4.12:test
[INFO] | +- org.assertj:assertj-core:jar:3.11.1:test
[INFO] | +- org.mockito:mockito-core:jar:2.23.4:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.9.7:test
[INFO] | | \- org.objenesis:objenesis:jar:2.6:test
[INFO] | +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] | +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-test:jar:5.1.4.RELEASE:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.6.2:test
[INFO] +- org.apache.httpcomponents:httpclient:jar:4.5.7:test
[INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.10:test
[INFO] | \- commons-codec:commons-codec:jar:1.11:test
[INFO] \- org.springframework.boot:spring-boot-devtools:jar:2.1.2.RELEASE:compile (optional)
[INFO] +- org.springframework.boot:spring-boot:jar:2.1.2.RELEASE:compile
[INFO] \- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.2.RELEASE:compile
3.弹簧支架
3.1 一个 REST 控制器创建以下 REST API 端点:
HTTP 方法 | 上呼吸道感染 | 描述 |
---|---|---|
得到 | /书籍 | 列出所有书籍。 |
邮政 | /书籍 | 存一本书。 |
得到 | /books/ | 找到一本 id = {:id}的书。 |
放 | /books/ | 在 id = {:id}处更新或保存图书。 |
修补 | /books/ | 更新单个字段,其中 id = {:id}。 |
删除 | /books/ | 删除 id = {:id}的图书。 |
BookController.java
package com.mkyong;
import com.mkyong.error.BookNotFoundException;
import com.mkyong.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
// Save
//return 201 instead of 200
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/books")
Book newBook(@RequestBody Book newBook) {
return repository.save(newBook);
}
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
// Save or update
@PutMapping("/books/{id}")
Book saveOrUpdate(@RequestBody Book newBook, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
x.setName(newBook.getName());
x.setAuthor(newBook.getAuthor());
x.setPrice(newBook.getPrice());
return repository.save(x);
})
.orElseGet(() -> {
newBook.setId(id);
return repository.save(newBook);
});
}
// update author only
@PatchMapping("/books/{id}")
Book patch(@RequestBody Map<String, String> update, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
String author = update.get("author");
if (!StringUtils.isEmpty(author)) {
x.setAuthor(author);
// better create a custom method to update a value = :newValue where id = :id
return repository.save(x);
} else {
throw new BookUnSupportedFieldPatchException(update.keySet());
}
})
.orElseGet(() -> {
throw new BookNotFoundException(id);
});
}
@DeleteMapping("/books/{id}")
void deleteBook(@PathVariable Long id) {
repository.deleteById(id);
}
}
4.春季数据 JPA
4.1 用 JPA 注释标注的模型。
Book.java
package com.mkyong;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String name;
private String author;
private BigDecimal price;
//setters, getters, constructors...
}
4.2 创建一个类并扩展JpaRepository
,它包含所有的 CRUD 操作。
BookRepository.java
package com.mkyong;
import org.springframework.data.jpa.repository.JpaRepository;
// Spring Data magic :)
public interface BookRepository extends JpaRepository<Book, Long> {
}
5.基本错误处理
创建一个@ControllerAdvice
来处理控制器抛出的定制异常。
CustomGlobalExceptionHandler
package com.mkyong.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Let Spring handle the exception, we just override the status code
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
@ExceptionHandler(BookUnSupportedFieldPatchException.class)
public void springUnSupportedFieldPatch(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.METHOD_NOT_ALLOWED.value());
}
}
BookNotFoundException.java
package com.mkyong.error;
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book id not found : " + id);
}
}
BookUnSupportedFieldPatchException.java
package com.mkyong.error;
import java.util.Set;
public class BookUnSupportedFieldPatchException extends RuntimeException {
public BookUnSupportedFieldPatchException(Set<String> keys) {
super("Field " + keys.toString() + " update is not allow.");
}
}
6.Spring Boot
6.1 创建一个@SpringBootApplication
类来启动 REST web 应用程序,并将 3 个预定义的 book 对象插入到 H2 内存数据库中进行演示。
StartBookApplication.java
package com.mkyong;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
@SpringBootApplication
public class StartBookApplication {
public static void main(String[] args) {
SpringApplication.run(StartBookApplication.class, args);
}
// init bean to insert 3 books into h2 database.
@Bean
CommandLineRunner initDatabase(BookRepository repository) {
return args -> {
repository.save(new Book("A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41")));
repository.save(new Book("The Life-Changing Magic of Tidying Up", "Marie Kondo", new BigDecimal("9.69")));
repository.save(new Book("Refactoring: Improving the Design of Existing Code", "Martin Fowler", new BigDecimal("47.99")));
};
}
}
7.演示
启动 Spring Boot 应用程序,用curl
命令测试 REST API 端点。
$ mvn spring-boot:run
7.1 查找全部—GET /books
> curl -v localhost:8080/books
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:33:57 GMT
<
[
{"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99}
]
7.2 找到一个—GET /books/1
> curl -v localhost:8080/books/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:37:26 GMT
<
{
"id":1,
"name":"A Guide to the Bodhisattva Way of Life",
"author":"Santideva",
"price":15.41
}
7.3 试验 404-GET /books/5
> curl -v localhost:8080/books/5
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books/5 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 404
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:38:45 GMT
<
{
"timestamp":"2019-02-19T06:38:45.743+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5"
}
7.4 测试保存—POST /books -d {json}
#Run this on Windows, need \"
> curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"Spring REST tutorials\",\"author\":\"mkyong\",\"price\":\"9.99\"}"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 65
>
* upload completely sent off: 65 out of 65 bytes
< HTTP/1.1 201
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:33:01 GMT
<
{
"id":4,
"name":
"Spring REST tutorials",
"author":"mkyong",
"price":9.99
}
7.5 测试更新—PUT /books/4 -d {json}
> curl -v -X PUT localhost:8080/books/4 -H "Content-type:application/json" -d "{\"name\":\"Spring Forever\",\"author\":\"pivotal\",\"price\":\"9.99\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PUT /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 59
>
* upload completely sent off: 59 out of 59 bytes
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:36:49 GMT
<
{
"id":4,
"name":"Spring Forever",
"author":"pivotal",
"price":9.99
}
> curl localhost:8080/books/4
{"id":4,"name":"Spring Forever","author":"pivotal","price":9.99}
7.6 测试更新“作者”字段—PATCH /books/4 -d {json}
> curl -v -X PATCH localhost:8080/books/4 -H "Content-type:application/json" -d "{\"author\":\"oracle\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PATCH /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 19
>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:39:53 GMT
{
"id":4,
"name":"Spring Forever",
"author":"oracle",
"price":9.99
}
7.6.1 测试更新“名称”字段—PATCH /books/4 -d {json}
> curl -v -X PATCH localhost:8080/books/4 -H "Content-type:application/json" -d "{\"name\":\"New Spring REST\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PATCH /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 26
>
* upload completely sent off: 26 out of 26 bytes
< HTTP/1.1 405
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:40:47 GMT
<
{
"timestamp":"2019-02-19T07:40:47.740+0000",
"status":405,
"error":"Method Not Allowed",
"message":"Field [name] update is not allow.",
"path":"/books/4"
}
7.7 测试删除-DELETE /books/4
> curl -v -X DELETE localhost:8080/books/4
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Length: 0
< Date: Tue, 19 Feb 2019 07:44:24 GMT
<
> curl localhost:8080/books/4
{
"timestamp":"2019-02-19T07:44:39.432+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 4",
"path":"/books/4"
}
> curl localhost:8080/books
[
{"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99}
]
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-hello-world
$ mvn spring-boot:run
参考
弹簧座集成测试示例
在本文中,我们将向您展示如何测试 Spring Boot REST 应用程序。通常,我们使用MockMvc
或TestRestTemplate
进行集成测试。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- maven3
- Java 8
pom.xml
<!-- spring integration test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring integration test for security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
1. MockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
1.1 本 Spring REST Hello World 示例的 CRUD 测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
//@WebMvcTest(BookController.class)
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "Book Name", "Mkyong", new BigDecimal("9.99"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_bookId_OK() throws Exception {
mockMvc.perform(get("/books/1"))
/*.andDo(print())*/
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Book Name")))
.andExpect(jsonPath("$.author", is("Mkyong")))
.andExpect(jsonPath("$.price", is(9.99)));
verify(mockRepository, times(1)).findById(1L);
}
@Test
public void find_allBook_OK() throws Exception {
List<Book> books = Arrays.asList(
new Book(1L, "Book A", "Ah Pig", new BigDecimal("1.99")),
new Book(2L, "Book B", "Ah Dog", new BigDecimal("2.99")));
when(mockRepository.findAll()).thenReturn(books);
mockMvc.perform(get("/books"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", is(1)))
.andExpect(jsonPath("$[0].name", is("Book A")))
.andExpect(jsonPath("$[0].author", is("Ah Pig")))
.andExpect(jsonPath("$[0].price", is(1.99)))
.andExpect(jsonPath("$[1].id", is(2)))
.andExpect(jsonPath("$[1].name", is("Book B")))
.andExpect(jsonPath("$[1].author", is("Ah Dog")))
.andExpect(jsonPath("$[1].price", is(2.99)));
verify(mockRepository, times(1)).findAll();
}
@Test
public void find_bookIdNotFound_404() throws Exception {
mockMvc.perform(get("/books/5")).andExpect(status().isNotFound());
}
@Test
public void save_book_OK() throws Exception {
Book newBook = new Book(1L, "Spring Boot Guide", "mkyong", new BigDecimal("2.99"));
when(mockRepository.save(any(Book.class))).thenReturn(newBook);
mockMvc.perform(post("/books")
.content(om.writeValueAsString(newBook))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
/*.andDo(print())*/
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Spring Boot Guide")))
.andExpect(jsonPath("$.author", is("mkyong")))
.andExpect(jsonPath("$.price", is(2.99)));
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void update_book_OK() throws Exception {
Book updateBook = new Book(1L, "ABC", "mkyong", new BigDecimal("19.99"));
when(mockRepository.save(any(Book.class))).thenReturn(updateBook);
mockMvc.perform(put("/books/1")
.content(om.writeValueAsString(updateBook))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("ABC")))
.andExpect(jsonPath("$.author", is("mkyong")))
.andExpect(jsonPath("$.price", is(19.99)));
}
@Test
public void patch_bookAuthor_OK() throws Exception {
when(mockRepository.save(any(Book.class))).thenReturn(new Book());
String patchInJson = "{\"author\":\"ultraman\"}";
mockMvc.perform(patch("/books/1")
.content(patchInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookPrice_405() throws Exception {
String patchInJson = "{\"price\":\"99.99\"}";
mockMvc.perform(patch("/books/1")
.content(patchInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(status().isMethodNotAllowed());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(0)).save(any(Book.class));
}
@Test
public void delete_book_OK() throws Exception {
doNothing().when(mockRepository).deleteById(1L);
mockMvc.perform(delete("/books/1"))
/*.andDo(print())*/
.andExpect(status().isOk());
verify(mockRepository, times(1)).deleteById(1L);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
1.2 此弹簧座验证的测试
package com.mkyong;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws Exception {
String bookInJson = "{\"name\":\"ABC\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(3)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a author")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a price")));
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws Exception {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(1)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")));
verify(mockRepository, times(0)).save(any(Book.class));
}
}
1.3 用@WithMockUser
对该弹簧座安全进行测试
package com.mkyong;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
//@WithMockUser(username = "USER")
@WithMockUser("USER")
@Test
public void find_login_ok() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("A Guide to the Bodhisattva Way of Life")))
.andExpect(jsonPath("$.author", is("Santideva")))
.andExpect(jsonPath("$.price", is(15.41)));
}
@Test
public void find_nologin_401() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isUnauthorized());
}
}
2.TestRestTemplate
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
@Autowired
private TestRestTemplate restTemplate;
2.1 本 Spring REST Hello World 示例的 CRUD 测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "Book Name", "Mkyong", new BigDecimal("9.99"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_bookId_OK() throws JSONException {
String expected = "{id:1,name:\"Book Name\",author:\"Mkyong\",price:9.99}";
ResponseEntity<String> response = restTemplate.getForEntity("/books/1", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
}
@Test
public void find_allBook_OK() throws Exception {
List<Book> books = Arrays.asList(
new Book(1L, "Book A", "Ah Pig", new BigDecimal("1.99")),
new Book(2L, "Book B", "Ah Dog", new BigDecimal("2.99")));
when(mockRepository.findAll()).thenReturn(books);
String expected = om.writeValueAsString(books);
ResponseEntity<String> response = restTemplate.getForEntity("/books", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findAll();
}
@Test
public void find_bookIdNotFound_404() throws Exception {
String expected = "{status:404,error:\"Not Found\",message:\"Book id not found : 5\",path:\"/books/5\"}";
ResponseEntity<String> response = restTemplate.getForEntity("/books/5", String.class);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void save_book_OK() throws Exception {
Book newBook = new Book(1L, "Spring Boot Guide", "mkyong", new BigDecimal("2.99"));
when(mockRepository.save(any(Book.class))).thenReturn(newBook);
String expected = om.writeValueAsString(newBook);
ResponseEntity<String> response = restTemplate.postForEntity("/books", newBook, String.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void update_book_OK() throws Exception {
Book updateBook = new Book(1L, "ABC", "mkyong", new BigDecimal("19.99"));
when(mockRepository.save(any(Book.class))).thenReturn(updateBook);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(om.writeValueAsString(updateBook), headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PUT, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(om.writeValueAsString(updateBook), response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookAuthor_OK() {
when(mockRepository.save(any(Book.class))).thenReturn(new Book());
String patchInJson = "{\"author\":\"ultraman\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(patchInJson, headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PUT, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookPrice_405() throws JSONException {
String expected = "{status:405,error:\"Method Not Allowed\",message:\"Field [price] update is not allow.\"}";
String patchInJson = "{\"price\":\"99.99\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(patchInJson, headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PATCH, entity, String.class);
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(0)).save(any(Book.class));
}
@Test
public void delete_book_OK() {
doNothing().when(mockRepository).deleteById(1L);
HttpEntity<String> entity = new HttpEntity<>(null, new HttpHeaders());
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.DELETE, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(mockRepository, times(1)).deleteById(1L);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.2 此弹簧座验证的测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws JSONException {
String bookInJson = "{\"name\":\"ABC\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
// send json with POST
ResponseEntity<String> response = restTemplate.postForEntity("/books", entity, String.class);
//printJSON(response);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\",\"Please provide a price\",\"Please provide a author\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws JSONException {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
//Try exchange
ResponseEntity<String> response = restTemplate.exchange("/books", HttpMethod.POST, entity, String.class);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.3 本弹簧座安全性测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
//@WithMockUser is not working with TestRestTemplate
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_login_ok() throws Exception {
String expected = "{id:1,name:\"A Guide to the Bodhisattva Way of Life\",author:\"Santideva\",price:15.41}";
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "password")
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void find_nologin_401() throws Exception {
String expected = "{\"status\":401,\"error\":\"Unauthorized\",\"message\":\"Unauthorized\",\"path\":\"/books/1\"}";
ResponseEntity<String> response = restTemplate
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ CD spring-rest-hello-world
$ mvn 测试
$ cd 弹簧支架-验证
$ mvn 测试
$ cd 弹簧-支架-安全
$ mvn 测试
参考
spring boot spring test unit test
弹簧座+弹簧安全示例
在本文中,我们将增强前面的 Spring REST 验证示例,通过添加 Spring Security 为请求的 URL(REST API 端点)执行认证和授权
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- Spring Security 5.1.3 .发布
- 春季数据 JPA 2.1.4.RELEASE
- H2 内存数据库 1.4.197
- Tomcat Embed 9.0.14
- JUnit 4.12
- maven3
- Java 8
1.项目目录
2.专家
包括弹簧安全的spring-boot-starter-security
和弹簧安全集成测试的spring-security-test
。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-rest-security</artifactId>
<packaging>jar</packaging>
<name>Spring Boot REST API Example</name>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<!-- Java 8 -->
<properties>
<java.version>1.8</java.version>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
<dependencies>
<!-- spring mvc, rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jpa, crud repository -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- in-memory database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- unit test rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- test patch operation need this -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
<scope>test</scope>
</dependency>
<!-- hot swapping, disable cache for template, enable live reload -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
</plugins>
</build>
</project>
项目依赖关系:
> mvn dependency:tree
[INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) @ spring-rest-security ---
[INFO] org.springframework.boot:spring-rest-security:jar:1.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.2.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.1:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.1:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.23:runtime
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.8:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.8:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.14:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.14:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.14:compile
[INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.14.Final:compile
[INFO] | | +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | \- com.fasterxml:classmate:jar:1.4.0:compile
[INFO] | +- org.springframework:spring-web:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-webmvc:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-context:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-expression:jar:5.1.4.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter-security:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework:spring-aop:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework.security:spring-security-config:jar:5.1.3.RELEASE:compile
[INFO] | \- org.springframework.security:spring-security-web:jar:5.1.3.RELEASE:compile
[INFO] +- org.springframework.security:spring-security-test:jar:5.1.3.RELEASE:test
[INFO] | +- org.springframework.security:spring-security-core:jar:5.1.3.RELEASE:compile
[INFO] | +- org.springframework:spring-core:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-test:jar:5.1.4.RELEASE:test
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.1.2.RELEASE:compile
[INFO] | | \- org.aspectj:aspectjweaver:jar:1.9.2:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:3.2.0:compile
[INFO] | | \- org.springframework:spring-jdbc:jar:5.1.4.RELEASE:compile
[INFO] | +- javax.transaction:javax.transaction-api:jar:1.3:compile
[INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile
[INFO] | | \- javax.activation:javax.activation-api:jar:1.2.0:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.3.7.Final:compile
[INFO] | | +- javax.persistence:javax.persistence-api:jar:2.2:compile
[INFO] | | +- org.javassist:javassist:jar:3.23.1-GA:compile
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.9.7:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.0.5.Final:compile
[INFO] | | +- org.dom4j:dom4j:jar:2.1.1:compile
[INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.4.Final:compile
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-tx:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | \- org.springframework:spring-aspects:jar:5.1.4.RELEASE:compile
[INFO] +- com.h2database:h2:jar:1.4.197:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.1.2.RELEASE:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | | \- net.minidev:json-smart:jar:2.3:test
[INFO] | | \- net.minidev:accessors-smart:jar:1.2:test
[INFO] | | \- org.ow2.asm:asm:jar:5.0.4:test
[INFO] | +- junit:junit:jar:4.12:test
[INFO] | +- org.assertj:assertj-core:jar:3.11.1:test
[INFO] | +- org.mockito:mockito-core:jar:2.23.4:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.9.7:test
[INFO] | | \- org.objenesis:objenesis:jar:2.6:test
[INFO] | +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] | +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.6.2:test
[INFO] +- org.apache.httpcomponents:httpclient:jar:4.5.7:test
[INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.10:test
[INFO] | \- commons-codec:commons-codec:jar:1.11:test
[INFO] \- org.springframework.boot:spring-boot-devtools:jar:2.1.2.RELEASE:compile (optional)
[INFO] +- org.springframework.boot:spring-boot:jar:2.1.2.RELEASE:compile
[INFO] \- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.2.RELEASE:compile
3.弹簧控制器
再次回顾一下 Book 控制器,稍后我们将集成 Spring Security 来保护 REST 端点。
BookController.java
package com.mkyong;
import com.mkyong.error.BookNotFoundException;
import com.mkyong.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;
@RestController
@Validated
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
// Save
@PostMapping("/books")
@ResponseStatus(HttpStatus.CREATED)
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable @Min(1) Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
// Save or update
@PutMapping("/books/{id}")
Book saveOrUpdate(@RequestBody Book newBook, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
x.setName(newBook.getName());
x.setAuthor(newBook.getAuthor());
x.setPrice(newBook.getPrice());
return repository.save(x);
})
.orElseGet(() -> {
newBook.setId(id);
return repository.save(newBook);
});
}
// update author only
@PatchMapping("/books/{id}")
Book patch(@RequestBody Map<String, String> update, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
String author = update.get("author");
if (!StringUtils.isEmpty(author)) {
x.setAuthor(author);
// better create a custom method to update a value = :newValue where id = :id
return repository.save(x);
} else {
throw new BookUnSupportedFieldPatchException(update.keySet());
}
})
.orElseGet(() -> {
throw new BookNotFoundException(id);
});
}
@DeleteMapping("/books/{id}")
void deleteBook(@PathVariable Long id) {
repository.deleteById(id);
}
}
P.S 此处未列出其他组件或库,请参考前面的弹簧座验证示例
4.春天安全
4.1 创建一个新的@Configuration
类并扩展WebSecurityConfigurerAdapter
。在下面的例子中,我们将使用 HTTP 基本认证来保护其余的端点。阅读注释,了解自我解释。
SpringSecurityConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
// Create 2 users for demo
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}password").roles("USER", "ADMIN");
}
// Secure the endpoins with HTTP Basic authentication
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//HTTP Basic authentication
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/books/**").hasRole("USER")
.antMatchers(HttpMethod.POST, "/books").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/books/**").hasRole("ADMIN")
.and()
.csrf().disable()
.formLogin().disable();
}
/*@Bean
public UserDetailsService userDetailsService() {
//ok for demo
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("USER", "ADMIN").build());
return manager;
}*/
}
4.2 完成后,上述 Spring REST API 端点受到 Spring Security 的保护🙂
阅读更多信息:
5.Spring Boot
正常的 Spring Boot 应用程序启动其余的端点,并插入 3 本书到 H2 数据库进行演示。
StartBookApplication
package com.mkyong;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
@SpringBootApplication
public class StartBookApplication {
// start everything
public static void main(String[] args) {
SpringApplication.run(StartBookApplication.class, args);
}
@Bean
CommandLineRunner initDatabase(BookRepository repository) {
return args -> {
repository.save(new Book("A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41")));
repository.save(new Book("The Life-Changing Magic of Tidying Up", "Marie Kondo", new BigDecimal("9.69")));
repository.save(new Book("Refactoring: Improving the Design of Existing Code", "Martin Fowler", new BigDecimal("47.99")));
};
}
}
6.演示
6.1 启动 Spring Boot 应用程序。
mvn spring-boot:run
6.2 一个正常的GET
和POST
会返回一个401
,所有端点都被保护,需要认证。
> curl localhost:8080/books
{
"timestamp":"2019-02-25T04:05:14.709+0000",
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/books"
}
> curl -X POST localhost:8080/books -H "Content-type:application/json"
-d {\"name\":\"ABC\",\"author\":\"mkyong\",\"price\":\"8.88\"}
{
"timestamp":"2019-02-25T04:11:17.150+0000",
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/books"
}
6.3 发送一个GET
请求和user
登录。
> curl localhost:8080/books -u user:password
[
{"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99}
]
> curl localhost:8080/books/1 -u admin:password
{
"id":1,
"name":"A Guide to the Bodhisattva Way of Life",
"author":"Santideva",
"price":15.41
}
6.4 尝试用“用户”登录发送一个POST
请求,它将返回403, Forbidden
错误。这是因为user
无权发送POST
请求。
> curl -X POST localhost:8080/books -H "Content-type:application/json"
-d {\"name\":\"ABC\",\"author\":\"mkyong\",\"price\":\"8.88\"} -u user:password
{
"timestamp":"2019-02-25T04:16:58.702+0000",
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/books"
}
再次检查 Spring 安全配置。要发送POST,PUT,PATCH or DELETE
请求,我们需要admin
SpringSecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//HTTP Basic authentication
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/books/**").hasRole("USER")
.antMatchers(HttpMethod.POST, "/books").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PATCH, "/books/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/books/**").hasRole("ADMIN")
.and()
.csrf().disable()
.formLogin().disable();
}
}
6.5 尝试使用admin
登录发送POST
请求
> curl -X POST localhost:8080/books -H "Content-type:application/json"
-d {\"name\":\"ABC\",\"author\":\"mkyong\",\"price\":\"8.88\"} -u admin:password
{
"id":4,
"name":"ABC",
"author":"mkyong",
"price":8.88
}
> curl localhost:8080/books -u user:password
[
{"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99},
{"id":4,"name":"ABC","author":"mkyong","price":8.88}
]
7.春季安全集成测试
7.1 用@WithMockUser
和MockMvc
进行测试
BookControllerTest.java
package com.mkyong;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
//@WithMockUser(username = "USER")
@WithMockUser("USER")
@Test
public void find_login_ok() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("A Guide to the Bodhisattva Way of Life")))
.andExpect(jsonPath("$.author", is("Santideva")))
.andExpect(jsonPath("$.price", is(15.41)));
}
@Test
public void find_nologin_401() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isUnauthorized());
}
}
7.2 用TestRestTemplate
进行测试
BookControllerRestTemplateTest.java
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
//@WithMockUser is not working with TestRestTemplate
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_login_ok() throws Exception {
String expected = "{id:1,name:\"A Guide to the Bodhisattva Way of Life\",author:\"Santideva\",price:15.41}";
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "password")
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void find_nologin_401() throws Exception {
String expected = "{\"status\":401,\"error\":\"Unauthorized\",\"message\":\"Unauthorized\",\"path\":\"/books/1\"}";
ResponseEntity<String> response = restTemplate
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
附注:@WithMockUser
不支持TestRestTemplate
,我们需要使用 . withBasicAuth
验证用户
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-security
$ mvn spring-boot:run
参考
- 春季安全参考
- Spring Boot 安全特征
- Hello Spring Security with Boot
- Spring 数据安全示例
- Spring Boot +朱尼特 5 +莫奇托
- 春歇你好天下例
- cURL–发布请求示例
- 维基百科–休息
- HTTP 基本认证
- StudentEndpointTest 示例
弹簧座验证示例
在本文中,我们将通过添加 bean 验证和自定义验证器来增强前面的 Spring REST Hello World 示例。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- maven3
- Java 8
1.控制器
再次查看之前的 REST 控制器:
BookController.java
package com.mkyong;
import com.mkyong.error.BookNotFoundException;
import com.mkyong.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
// Save
@PostMapping("/books")
@ResponseStatus(HttpStatus.CREATED)
Book newBook(@RequestBody Book newBook) {
return repository.save(newBook);
}
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}
2.Bean 验证(Hibernate 验证器)
2.1 如果任何 JSR-303 实现(如 Hibernate Validator)在类路径上可用,bean 验证将被自动启用。默认情况下,Spring Boot 会自动获取并下载 Hibernate 验证程序。
2.2 下面的 POST 请求将被通过,我们需要在book
对象上实现 bean 验证,以确保像name
、author
和price
这样的字段不为空。
@PostMapping("/books")
Book newBook(@RequestBody Book newBook) {
return repository.save(newBook);
}
curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"ABC\"}"
2.3 用[javax.validation.constraints.*](http://web.archive.org/web/20220904221328/https://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/javax/validation/constraints/package-summary.html)
注释对 bean 进行注释。
Book.java
package com.mkyong;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Please provide a name")
private String name;
@NotEmpty(message = "Please provide a author")
private String author;
@NotNull(message = "Please provide a price")
@DecimalMin("1.00")
private BigDecimal price;
//...
}
2.4 在@RequestBody
中增加@Valid
。完成,现在启用 bean 验证。
BookController.java
import javax.validation.Valid;
@RestController
public class BookController {
@PostMapping("/books")
Book newBook(@Valid @RequestBody Book newBook) {
return repository.save(newBook);
}
//...
}
2.5 尝试再次向 REST 端点发送 POST 请求。如果 bean 验证失败,将触发一个MethodArgumentNotValidException
。默认情况下,Spring 会发回一个 HTTP 状态 400 错误请求,但是没有错误细节。
curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"ABC\"}"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: */*
> Content-type:application/json
> Content-Length: 32
>
* upload completely sent off: 32 out of 32 bytes
< HTTP/1.1 400
< Content-Length: 0
< Date: Wed, 20 Feb 2019 13:02:30 GMT
< Connection: close
<
2.6 上面的错误响应不友好,我们可以捕捉MethodArgumentNotValidException
并像这样覆盖响应:
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// error handle for @Valid
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
//Get all errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
2.7 再试一次。完成了。
curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"ABC\"}"
{
"timestamp":"2019-02-20T13:21:27.653+0000",
"status":400,
"errors":[
"Please provide a author",
"Please provide a price"
]
}
3.路径变量验证
3.1 我们也可以直接在路径变量甚至请求参数上应用javax.validation.constraints.*
注释。
3.2 在类级别应用@Validated
,并在路径变量上添加javax.validation.constraints.*
注释,如下所示:
BookController.java
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
@RestController
@Validated // class level
public class BookController {
@GetMapping("/books/{id}")
Book findOne(@PathVariable @Min(1) Long id) { //jsr 303 annotations
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}
3.3 默认的错误信息是好的,只是错误代码 500 不合适。
curl -v localhost:8080/books/0
{
"timestamp":"2019-02-20T13:27:43.638+0000",
"status":500,
"error":"Internal Server Error",
"message":"findOne.id: must be greater than or equal to 1",
"path":"/books/0"
}
3.4 如果@Validated
失败,将触发一个ConstraintViolationException
,我们可以这样覆盖错误代码:
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
//..
}
curl -v localhost:8080/books/0
{
"timestamp":"2019-02-20T13:35:59.808+0000",
"status":400,
"error":"Bad Request",
"message":"findOne.id: must be greater than or equal to 1",
"path":"/books/0"
}
4.自定义验证程序
4.1 我们将为author
字段创建一个自定义验证器,只允许 4 个作者保存到数据库中。
Author.java
package com.mkyong.error.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = AuthorValidator.class)
@Documented
public @interface Author {
String message() default "Author is not allowed.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
AuthorValidator.java
package com.mkyong.error.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class AuthorValidator implements ConstraintValidator<Author, String> {
List<String> authors = Arrays.asList("Santideva", "Marie Kondo", "Martin Fowler", "mkyong");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return authors.contains(value);
}
}
Book.java
package com.mkyong;
import com.mkyong.error.validator.Author;
import javax.persistence.Entity;
import javax.validation.constraints.NotEmpty;
//...
@Entity
public class Book {
@Author
@NotEmpty(message = "Please provide a author")
private String author;
//...
4.2 测试一下。如果自定义验证器失败,它将触发一个MethodArgumentNotValidException
curl -v -X POST localhost:8080/books
-H "Content-type:application/json"
-d "{\"name\":\"Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}"
{
"timestamp":"2019-02-20T13:49:59.971+0000",
"status":400,
"errors":["Author is not allowed."]
}
5.弹簧综合测试
5.1 用MockMvc
进行测试
BookControllerTest.java
package com.mkyong;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws Exception {
String bookInJson = "{\"name\":\"ABC\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(3)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a author")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a price")));
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws Exception {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(1)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")));
verify(mockRepository, times(0)).save(any(Book.class));
}
}
5.2 用TestRestTemplate
进行测试
BookControllerRestTemplateTest.java
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws JSONException {
String bookInJson = "{\"name\":\"ABC\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
// send json with POST
ResponseEntity<String> response = restTemplate.postForEntity("/books", entity, String.class);
//printJSON(response);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\",\"Please provide a price\",\"Please provide a author\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws JSONException {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
//Try exchange
ResponseEntity<String> response = restTemplate.exchange("/books", HttpMethod.POST, entity, String.class);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-validation
$ mvn spring-boot:run
参考
- javax . validation . constraints
- Spring Boot 验证功能
- Spring Boot +朱尼特 5 +莫奇托
- 春歇你好天下例
- cURL–发布请求示例
- 维基百科–休息
Spring 安全访问控制示例
在 Spring Security 中,访问控制或授权很容易实现。请参见以下代码片段:
<http auto-config="true">
<intercept-url pattern="/admin*" access="ROLE_ADMIN" />
</http>
也就是说,只有权限为“ ROLE_ADMIN 的用户才能访问 URI /admin* 。如果非授权用户试图访问它,将显示“ http 403 访问被拒绝页面”。
Spring EL + Access Control
See equivalent version in Spring EL. It is more flexible and contains many useful ready made functions like “hasIpAddress“, make sure check all available el functions in this official Spring el access control documentation.
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/admin*" access="hasRole('ROLE_ADMIN')" />
</http>
在本教程中,我们将向您展示如何使用 Spring Security 来实现对 URL“**/ADMIN ***”的访问控制,其中只有获得“ ROLE_ADMIN ”授权的用户才可以访问该页面。
1.项目相关性
访问控制包含在核心 Spring 安全 jar 中。请参考这个 Spring Security hello world 示例以获得所需依赖项的列表。
2.Spring MVC
Spring MVC 控制器并返回一个“hello”视图,这应该是不言自明的。
文件:WelcomeController.java
package com.mkyong.common.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class WelcomeController {
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String welcomeAdmin(ModelMap model) {
model.addAttribute("message", "Spring Security - ROLE_ADMIN");
return "hello";
}
}
文件:hello.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2>Message : ${message}</h2>
<a href="<c:url value="j_spring_security_logout" />" > Logout</a>
</body>
</html>
3.春天安全
完整的 Spring 安全配置,只允许用户" eclipse "访问" /admin "页面。
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">
<http auto-config="true">
<intercept-url pattern="/admin*" access="ROLE_ADMIN" />
<logout logout-success-url="/admin" />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="password" authorities="ROLE_USER" />
<user name="eclipse" password="password" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
4.演示
网址:http://localhost:8080/spring MVC/admin
1.将显示默认登录表单。
2.如果用户“ mkyong ”登录,会显示“ http 403 被拒绝访问页面,因为“ mkyong ”是“ ROLE_USER ”。
3.如果用户“ eclipse ”登录,会显示“【hello.jsp】T2,因为“ eclipse ”是“ ROLE_ADMIN ”。
Customize 403 page
Default 403 page is ugly, read this example – How to customize http 403 access denied page in spring security.
下载源代码
Download it – Spring-Security-Access-Control-Example.zip (10 KB)
参考
access control spring security (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190215001531/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Spring Security:检查用户是否来自“记住我”cookie
这个 Spring 安全示例向您展示了如何检查一个用户是否从“remember me”cookie 登录。
private boolean isRememberMeAuthenticated() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass());
}
@RequestMapping(value = "/admin/update**", method = RequestMethod.GET)
public ModelAndView updatePage() {
ModelAndView model = new ModelAndView();
if (isRememberMeAuthenticated()) {
model.setViewName("/login");
} else {
model.setViewName("update");
}
return model;
}
在 Spring 安全标记中,您可以这样编码:
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<%@page session="true"%>
<html>
<body>
<sec:authorize access="isRememberMe()">
<h2># This user is login by "Remember Me Cookies".</h2>
</sec:authorize>
<sec:authorize access="isFullyAuthenticated()">
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2># This user is login by username / password.</h2>
</sec:authorize>
</body>
</html>
Note
isRememberMe() – Returns true if the current principal is a remember-me user
isFullyAuthenticated() – Returns true if the user is not an anonymous or a remember-me user ## 参考
spring security (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190304003401/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Spring 安全自定义登录表单注释示例
在本教程中,我们将把之前的 Spring Security 定制登录表单(XML) 项目转换成一个纯粹的基于注释的项目。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
- Tomcat 7 (Servlet 3.x)
Note
In this example, last Spring Security hello world annotation example will be reused, enhance it to support a custom login form.
1.项目演示
//web.archive.org/web/20190303104611if_/http://www.youtube.com/embed/Th5keN9Cqx4
2.目录结构
查看本教程的最终目录结构。
## 3.Spring 安全配置
通过注释进行 Spring 安全配置。
SecurityConfig.java
package com.mkyong.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("mkyong").password("123456").roles("USER");
}
//.csrf() is optional, enabled by default, if using WebSecurityConfigurerAdapter constructor
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_USER')")
.and()
.formLogin().loginPage("/login").failureUrl("/login?error")
.usernameParameter("username").passwordParameter("password")
.and()
.logout().logoutSuccessUrl("/login?logout")
.and()
.csrf();
}
}
相当于 Spring Security XML 文件:
<http auto-config="true">
<intercept-url pattern="/admin**" access="ROLE_USER" />
<form-login
login-page="/login"
default-target-url="/welcome"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password" />
<logout logout-success-url="/login?logout" />
<!-- enable csrf protection -->
<csrf/>
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="123456" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
4.自定义登录表单
4.1 显示自定义登录表单的页面。如果启用了 CSRF 保护,记得在登录和注销表单中添加${_csrf.token}
。
login.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Login Page</title>
<style>
.error {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.msg {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
#login-box {
width: 300px;
padding: 20px;
margin: 100px auto;
background: #fff;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border: 1px solid #000;
}
</style>
</head>
<body onload='document.loginForm.username.focus();'>
<h1>Spring Security Custom Login Form (Annotation)</h1>
<div id="login-box">
<h2>Login with Username and Password</h2>
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>
<form name='loginForm'
action="<c:url value='j_spring_security_check' />" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='user' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='pass' /></td>
</tr>
<tr>
<td colspan='2'>
<input name="submit" type="submit" value="submit" />
</td>
</tr>
</table>
<input type="hidden"
name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
</div>
</body>
</html>
4.2 显示欢迎消息的页面,默认页面。
hello.jsp
<%@page session="false"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
</body>
</html>
4.3 本页面受密码保护,只有通过认证的用户才可以访问。
admin.jsp + logout
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:url value="/j_spring_security_logout" var="logoutUrl" />
<!-- csrt support -->
<form action="${logoutUrl}" method="post" id="logoutForm">
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
<script>
function formSubmit() {
document.getElementById("logoutForm").submit();
}
</script>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
Welcome : ${pageContext.request.userPrincipal.name} | <a
href="javascript:formSubmit()"> Logout</a>
</h2>
</c:if>
</body>
</html>
5.Spring MVC 控制器
简单的控制器。
HelloController.java
package com.mkyong.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView welcomePage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Custom Login Form");
model.addObject("message", "This is welcome page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Custom Login Form");
model.addObject("message", "This is protected page!");
model.setViewName("admin");
return model;
}
//Spring Security see this :
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout) {
ModelAndView model = new ModelAndView();
if (error != null) {
model.addObject("error", "Invalid username and password!");
}
if (logout != null) {
model.addObject("msg", "You've been logged out successfully.");
}
model.setViewName("login");
return model;
}
}
6.初始值设定项类
下面是初始化器类,使它成为一个纯粹的基于注释的项目。
6.1 启用 Spring 安全配置的初始化器类。
SpringSecurityInitializer.java
package com.mkyong.config.core;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
6.2 启用 Spring MVC 的初始化器类。
SpringMvcInitializer.java
package com.mkyong.config.core;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import com.mkyong.config.AppConfig;
public class SpringMvcInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { AppConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AppConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration
@ComponentScan({ "com.mkyong.web.*" })
@Import({ SecurityConfig.class })
public class AppConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver
= new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/pages/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
7.演示
7.1.欢迎页面-http://localhost:8080/spring-security-loginform-annotation/
7.2 尝试访问/admin
页面,显示您的自定义登录表单。
7.3.如果用户名和密码不正确,显示/login?error
。
7.4.如果用户名和密码正确,Spring 会将请求重定向到最初请求的 URL 并显示页面。
7.5.尝试注销,会重定向到/login?logout
页面。
下载源代码
Download it – spring-security-custom-login-form-annotation.zip (19 KB)
参考
Spring Security:编码的密码看起来不像 BCrypt
在 Spring Security 中,使用 bcrypt 密码散列的数据库认证。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
//...
String password = "123456";
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String hashedPassword = passwordEncoder.encode(password);
spring-security.xml
<authentication-manager>
<authentication-provider>
<password-encoder hash="bcrypt" />
//...
</authentication-provider>
</authentication-manager>
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(45) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username));
查看调试输出,总是说“编码的密码看起来不像 BCrypt ”,即使提供了正确的密码。
//...
12:56:31.868 DEBUG o.s.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
12:56:31.868 WARN o.s.s.c.bcrypt.BCryptPasswordEncoder - Encoded password does not look like BCrypt
12:56:31.868 DEBUG o.s.s.a.d.DaoAuthenticationProvider - Authentication failed: password does not match stored value
解决办法
例如,在 bcrypt 散列算法中,每次都会生成长度为 60 的不同散列值
$2a$10$LOqePml/koRGsk2YAIOFI.1YNKZg7EsQ5BAIuYP1nWOyYRl21dlne
一个常见的错误,“密码”列(users 表)的长度小于 60,比如password VARCHAR(45)
,有些数据库会自动截断数据。因此,您总是得到警告“编码的密码看起来不像 BCrypt”。
要解决,请确保“密码”栏的长度至少为 60。
bcryt hashing spring security (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190309053253/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Spring Security 自定义登录表单示例
默认情况下,如果没有指定登录表单,Spring Security 将自动创建一个默认的登录表单。请参考这个——春安 hello world 示例。
在本教程中,我们将向您展示如何为 Spring Security 创建一个定制的登录表单(XML 示例)。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
Note
In this example, previous Spring Security hello world example will be reused, enhance it to support a custom login form.
1.项目演示
//web.archive.org/web/20220309221809if_/https://www.youtube.com/embed/Y0QaOxnEn1Y
2.目录结构
查看本教程的最终目录结构。
3.Spring 安全配置
在 Spring XML 文件中定义了您的自定义登录表单。参见下面的解释:
- log in-page = "/log in "–显示自定义登录表单的页面
- 认证-失败-url="/login?错误”-如果验证失败,请转到第
/login?error
页 - 注销-成功-URL = "/登录?注销”——如果注销成功,转到视图
/logout
- username-parameter = " username "–包含“用户名”的请求的名称。在 HTML 中,这是输入文本的名称。
–启用跨站请求伪造(CSRF)保护,参见此链接。在 XML 中,默认情况下,CSRF 保护是禁用的。
通常,我们不涉及登录或注销等认证处理,让 Spring 来处理,我们只处理成功或失败的页面来显示。
spring-security.xml
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<http auto-config="true">
<intercept-url pattern="/admin**" access="ROLE_USER" />
<form-login
login-page="/login"
default-target-url="/welcome"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password" />
<logout logout-success-url="/login?logout" />
<!-- enable csrf protection -->
<csrf/>
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="123456" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
在上面的祝贺中,/admin
及其子文件夹都有密码保护。
Cross Site Request Forgery (CSRF) Protection
If CSRF is enabled, you have to include a _csrf.token
in the page you want to login or logout. Refer to below login.jsp
and admin.jsp
(logout form). Otherwise, both login and logout function will be failed.Password in clear-text?
A pretty bad idea, you should always hash the password with SHA algorithm, this tutorial show you how – Spring Security password hashing example.
4.自定义登录表单
一个定制的登录表单来匹配上面(步骤 3)的 Spring Security 祝贺。应该是不言自明的。
login.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Login Page</title>
<style>
.error {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.msg {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
#login-box {
width: 300px;
padding: 20px;
margin: 100px auto;
background: #fff;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border: 1px solid #000;
}
</style>
</head>
<body onload='document.loginForm.username.focus();'>
<h1>Spring Security Custom Login Form (XML)</h1>
<div id="login-box">
<h2>Login with Username and Password</h2>
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>
<form name='loginForm'
action="<c:url value='j_spring_security_check' />" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td colspan='2'><input name="submit" type="submit"
value="submit" /></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
</div>
</body>
</html>
而另外两个 JSP 页面,btw admin.jsp
是受 Spring Security 密码保护的。
hello.jsp
<%@page session="false"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
</body>
</html>
admin.jsp + logout
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:url value="/j_spring_security_logout" var="logoutUrl" />
<!-- csrt for log out-->
<form action="${logoutUrl}" method="post" id="logoutForm">
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
<script>
function formSubmit() {
document.getElementById("logoutForm").submit();
}
</script>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
Welcome : ${pageContext.request.userPrincipal.name} | <a
href="javascript:formSubmit()"> Logout</a>
</h2>
</c:if>
</body>
</html>
5.Spring MVC 控制器
简单的控制器。
HelloController.java
package com.mkyong.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView welcomePage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Custom Login Form");
model.addObject("message", "This is welcome page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Custom Login Form");
model.addObject("message", "This is protected page!");
model.setViewName("admin");
return model;
}
//Spring Security see this :
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout) {
ModelAndView model = new ModelAndView();
if (error != null) {
model.addObject("error", "Invalid username and password!");
}
if (logout != null) {
model.addObject("msg", "You've been logged out successfully.");
}
model.setViewName("login");
return model;
}
}
6.演示
6.1.欢迎页面–http://localhost:8080/spring-security-loginform-XML/
6.2 尝试访问/admin
页面,Spring Security 会拦截请求并重定向到/login
,显示您的自定义登录表单。
6.3.如果用户名和密码不正确,就会显示错误消息,Spring 会重定向到这个 URL /login?error
。
6.4.如果用户名和密码正确,Spring 将重定向到最初请求的 URL 并显示页面。
6.5.尝试注销,会重定向到/login?logout
页面。
下载源代码
Download it – spring-security-custom-login-form-xml.zip (15 KB)
参考
使用数据库的 Spring 安全表单登录
在本教程中,我们将向您展示如何在 Spring Security 中执行数据库认证(使用 XML 和注释)。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- 春季 JDBC 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
- Tomcat 6 或 7 (Servlet 3.x)
- MySQL 服务器 5.6
以前的登录表单内存认证将被重用,增强以支持以下特性:
- 数据库认证,使用 Spring-JDBC 和 MySQL。
- Spring Security,JSP TagLib,
sec:authorize access="hasRole('ROLE_USER')
- 自定义 403 拒绝访问页面。
1.项目演示
//web.archive.org/web/20220323160657if_/https://www.youtube.com/embed/2ms57c2EdUg
2.项目目录
查看最终项目结构(基于 XML):
查看最终项目结构(基于注释):
3.项目相关性
获取对 Spring、Spring Security、JDBC、Taglib 和 MySQL 的依赖
pom.xml
<properties>
<jdk.version>1.6</jdk.version>
<spring.version>3.2.8.RELEASE</spring.version>
<spring.security.version>3.2.3.RELEASE</spring.security.version>
<jstl.version>1.2</jstl.version>
<mysql.connector.version>5.1.30</mysql.connector.version>
</properties>
<dependencies>
<!-- Spring 3 dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<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>
<!-- 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>
<!-- Spring Security JSP Taglib -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.security.version}</version>
</dependency>
<!-- jstl for jsp page -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<!-- connect to mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
</dependencies>
</project>
4.数据库ˌ资料库
要执行数据库身份验证,您必须创建表来存储用户和角色的详细信息。请参考这个 Spring 安全用户模式参考。下面是创建users
和user_roles
表的 MySQL 脚本。
4.1 创建一个“用户”表。
users.sql
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(45) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username));
4.2 创建一个“用户角色”表。
user_roles.sql
CREATE TABLE user_roles (
user_role_id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
role varchar(45) NOT NULL,
PRIMARY KEY (user_role_id),
UNIQUE KEY uni_username_role (role,username),
KEY fk_username_idx (username),
CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username));
4.3 插入一些测试记录。
INSERT INTO users(username,password,enabled)
VALUES ('mkyong','123456', true);
INSERT INTO users(username,password,enabled)
VALUES ('alex','123456', true);
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_USER');
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_ADMIN');
INSERT INTO user_roles (username, role)
VALUES ('alex', 'ROLE_USER');
Note
- 用户名“mkyong”,角色 _ 用户和角色 _ 管理员。
- 用户名“alexa”,带角色 _ 用户。
5.Spring 安全配置
XML 和注释中的 Spring 安全性。
5.1 创建一个数据源来连接 MySQL。
spring-database.xml
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="password" />
</bean>
</beans>
相当于弹簧注释:
SecurityConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration
@ComponentScan({ "com.mkyong.web.*" })
@Import({ SecurityConfig.class })
public class AppConfig {
@Bean(name = "dataSource")
public DriverManagerDataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/test");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("password");
return driverManagerDataSource;
}
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/pages/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
5.2 使用jdbc-user-service
定义一个执行数据库认证的查询。
spring-security.xml
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<!-- enable use-expressions -->
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
<!-- access denied page -->
<access-denied-handler error-page="/403" />
<form-login
login-page="/login"
default-target-url="/welcome"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password" />
<logout logout-success-url="/login?logout" />
<!-- enable csrf protection -->
<csrf/>
</http>
<!-- Select users and user_roles from database -->
<authentication-manager>
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query=
"select username,password, enabled from users where username=?"
authorities-by-username-query=
"select username, role from user_roles where username =? " />
</authentication-provider>
</authentication-manager>
</beans:beans>
相当于 Spring 安全注释:
SecurityConfig.java
package com.mkyong.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery(
"select username,password, enabled from users where username=?")
.authoritiesByUsernameQuery(
"select username, role from user_roles where username=?");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.and()
.formLogin().loginPage("/login").failureUrl("/login?error")
.usernameParameter("username").passwordParameter("password")
.and()
.logout().logoutSuccessUrl("/login?logout")
.and()
.exceptionHandling().accessDeniedPage("/403")
.and()
.csrf();
}
}
6.JSP 页面
定制登录页面的 JSP 页面。
6.1 默认页面,展示使用 Spring Security JSP taglib sec:authorize
向拥有“ ROLE_USER 权限的用户显示内容。
hello.jsp
<%@taglib prefix="sec"
uri="http://www.springframework.org/security/tags"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<sec:authorize access="hasRole('ROLE_USER')">
<!-- For login user -->
<c:url value="/j_spring_security_logout" var="logoutUrl" />
<form action="${logoutUrl}" method="post" id="logoutForm">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
<script>
function formSubmit() {
document.getElementById("logoutForm").submit();
}
</script>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
User : ${pageContext.request.userPrincipal.name} | <a
href="javascript:formSubmit()"> Logout</a>
</h2>
</c:if>
</sec:authorize>
</body>
</html>
6.2 显示自定义登录表单的页面。
login.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<head>
<title>Login Page</title>
<style>
.error {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.msg {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
#login-box {
width: 300px;
padding: 20px;
margin: 100px auto;
background: #fff;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border: 1px solid #000;
}
</style>
</head>
<body onload='document.loginForm.username.focus();'>
<h1>Spring Security Login Form (Database Authentication)</h1>
<div id="login-box">
<h2>Login with Username and Password</h2>
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>
<form name='loginForm'
action="<c:url value='/j_spring_security_check' />" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username'></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td colspan='2'><input name="submit" type="submit"
value="submit" /></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
</div>
</body>
</html>
6.3 该页面受密码保护,只有经过认证的用户“ ROLE_ADMIN ”可以访问。
admin.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:url value="/j_spring_security_logout" var="logoutUrl" />
<form action="${logoutUrl}" method="post" id="logoutForm">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
<script>
function formSubmit() {
document.getElementById("logoutForm").submit();
}
</script>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
Welcome : ${pageContext.request.userPrincipal.name} | <a
href="javascript:formSubmit()"> Logout</a>
</h2>
</c:if>
</body>
</html>
6.4 自定义 403 拒绝访问页面。
403.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<h1>HTTP Status 403 - Access is denied</h1>
<c:choose>
<c:when test="${empty username}">
<h2>You do not have permission to access this page!</h2>
</c:when>
<c:otherwise>
<h2>Username : ${username} <br/>
You do not have permission to access this page!</h2>
</c:otherwise>
</c:choose>
</body>
</html>
7.Spring MVC 控制器
简单的控制器。
MainController.java
package com.mkyong.web.controller;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class MainController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView defaultPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Login Form - Database Authentication");
model.addObject("message", "This is default page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Login Form - Database Authentication");
model.addObject("message", "This page is for ROLE_ADMIN only!");
model.setViewName("admin");
return model;
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout) {
ModelAndView model = new ModelAndView();
if (error != null) {
model.addObject("error", "Invalid username and password!");
}
if (logout != null) {
model.addObject("msg", "You've been logged out successfully.");
}
model.setViewName("login");
return model;
}
//for 403 access denied page
@RequestMapping(value = "/403", method = RequestMethod.GET)
public ModelAndView accesssDenied() {
ModelAndView model = new ModelAndView();
//check if user is login
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!(auth instanceof AnonymousAuthenticationToken)) {
UserDetails userDetail = (UserDetails) auth.getPrincipal();
model.addObject("username", userDetail.getUsername());
}
model.setViewName("403");
return model;
}
}
8.演示
8.1.默认页面
XML—http://localhost:8080/spring-security-loginform-database/
Annotation—http://localhost:8080/spring-security-loginform-database-Annotation/
8.2 尝试访问/admin
页面,只允许“mkyong”ROLE _ ADMIN访问。
8.3.如果“alex”试图访问/admin
,则显示 403 访问被拒绝页面。
8.3 "alex "在默认页面中显示sec:authorize
的用法
8.4.如果“mkyong”试图访问/admin
,将显示管理页面。
下载源代码
Download XML version – spring-security-login-form-database-xml.zip (16 KB)Download Annotation version – spring-security-login-form-database-annotation.zip (22 KB)
参考
- Spring 安全参考–JDBC-ser-service
- Spring 安全参考–juser-schema
- Spring 安全参考–标签库
- 春安你好世界注释示例
- 创建自定义登录表单
- Spring Security–如何定制 403 拒绝访问页面
Spring Security Hello World 注释示例
在 preview post 中,我们使用 XML 文件来配置 Spring MVC 环境中的 Spring 安全性。在本教程中,我们将向您展示如何将之前基于 XML 的 Spring Security 项目转换为一个纯 Spring 注释项目。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
- Tomcat 7 (Servlet 3.x)
几个音符
- 本教程使用
WebApplicationInitializer
来自动加载 Spring Context Loader,它仅在 Servlet 3.x 容器中受支持,例如 Tomcat 7 和 Jetty 8。 - 因为我们使用的是
WebApplicationInitializer
,所以不需要web.xml
文件。 - 旧的 Servlet 2.x 容器支持 Spring 安全注释,例如 Tomcat 6。如果使用经典的 XML 文件加载 Spring 上下文,本教程仍然能够部署在 Servlet 2.x 容器上,例如 Tomcat 6
1.项目演示
看看效果如何。
//web.archive.org/web/20190304011458if_/http://www.youtube.com/embed/NKamWA6hDaU
2.目录结构
查看本教程的最终目录结构。
## 3.Spring 安全依赖项
要使用 Spring security,需要spring-security-web
和spring-security-config
。
pom.xml
<properties>
<jdk.version>1.6</jdk.version>
<spring.version>3.2.8.RELEASE</spring.version>
<spring.security.version>3.2.3.RELEASE</spring.security.version>
<jstl.version>1.2</jstl.version>
</properties>
<dependencies>
<!-- Spring 3 dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</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>
<!-- jstl for jsp page -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
</dependencies>
4.Spring MVC Web 应用程序
一个简单的控制器:
- 如果 URL =
/welcome
或/
,返回 hello 页面。 - 如果 URL =
/admin
,返回管理页面。 - 如果 URL =
/dba
,返回管理页面。
稍后,我们将保护/admin
和/dba
URL。
HelloController.java
package com.mkyong.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView welcomePage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Hello World");
model.addObject("message", "This is welcome page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Hello World");
model.addObject("message", "This is protected page - Admin Page!");
model.setViewName("admin");
return model;
}
@RequestMapping(value = "/dba**", method = RequestMethod.GET)
public ModelAndView dbaPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Hello World");
model.addObject("message", "This is protected page - Database Page!");
model.setViewName("admin");
return model;
}
}
两个 JSP 页面。
hello.jsp
<%@page session="false"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
</body>
</html>
admin.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>Welcome : ${pageContext.request.userPrincipal.name}
| <a href="<c:url value="/logout" />" > Logout</a></h2>
</c:if>
</body>
</html>
5.Spring 安全配置
5.1 创建一个 Spring 安全配置文件,并用@EnableWebSecurity
标注
SecurityConfig.java
package com.mkyong.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("mkyong").password("123456").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
auth.inMemoryAuthentication().withUser("dba").password("123456").roles("DBA");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')")
.and().formLogin();
}
}
相当于 Spring Security xml 文件:
<http auto-config="true">
<intercept-url pattern="/admin**" access="ROLE_ADMIN" />
<intercept-url pattern="/dba**" access="ROLE_ADMIN,ROLE_DBA" />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="123456" authorities="ROLE_USER" />
<user name="admin" password="123456" authorities="ROLE_ADMIN" />
<user name="dba" password="123456" authorities="ROLE_DBA" />
</user-service>
</authentication-provider>
</authentication-manager>
5.2 创建一个类扩展AbstractSecurityWebApplicationInitializer
,它会自动加载springSecurityFilterChain
。
SpringSecurityInitializer.java
package com.mkyong.config.core;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//do nothing
}
在web.xml
文件中相当于 Spring Security:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
6.Spring MVC 配置
6.1 一个 Config 类,定义视图的技术和导入上面的SecurityConfig.java
。
AppConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration
@ComponentScan({ "com.mkyong.web.*" })
@Import({ SecurityConfig.class })
public class AppConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver
= new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/pages/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
相当于 Spring XML 文件:
<context:component-scan base-package="com.mkyong.web.*" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/pages/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
6.2 创建一个Initializer
类,来加载一切。
SpringMvcInitializer.java
package com.mkyong.config.core;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import com.mkyong.config.AppConfig;
public class SpringMvcInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { AppConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
完成了。
Note
In Servlet 3.x container environment + Spring container will detect and loads the Initializer
classes automatically.
7.演示
7.1.欢迎页面–http://localhost:8080/spring-security-hello world-annotation/welcome
7.2 尝试访问/admin
页面,Spring Security 会拦截请求并重定向到/login
,显示默认登录表单。
7.3.如果用户名和密码不正确,就会显示错误消息,Spring 会重定向到这个 URL /login?error
。
7.4.如果用户名和密码正确,Spring 会将请求重定向到最初请求的 URL 并显示页面。
7.5.对于未授权用户,Spring 将显示 403 拒绝访问页面。例如,用户“mkyong”或“dba”试图访问/admin
URL。
下载源代码
Download it – spring-security-helloworld-annotation.zip (12 KB)
参考
- 春天的安全
- Spring Security Java Config Preview:Web Security
- Hello Spring MVC 安全 Java 配置
- 维基百科:Java Servlet
- 维基百科:阿帕奇雄猫
- Spring Security Hello World XML 示例
Spring Security hello world 示例
在本教程中,我们将向您展示如何将 Spring Security 与 Spring MVC web 应用程序集成在一起,以保护 URL 访问。在实现了 Spring Security 之后,要访问“管理”页面的内容,用户需要输入正确的“用户名”和“密码”。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
Note
Spring Security 3.0 requires Java 5.0 Runtime Environment or higher
1.项目演示
//web.archive.org/web/20210223165920if_/https://www.youtube.com/embed/hblHPyMuHJc
freestar.config.enabled_slots.push({ placementName: "mkyong_incontent_1", slotId: "mkyong_incontent_1" });
2.目录结构
查看本教程的最终目录结构。
3.Spring 安全依赖项
要使用 Spring security,需要spring-security-web
和spring-security-config
。
pom.xml
<properties>
<jdk.version>1.6</jdk.version>
<spring.version>3.2.8.RELEASE</spring.version>
<spring.security.version>3.2.3.RELEASE</spring.security.version>
<jstl.version>1.2</jstl.version>
</properties>
<dependencies>
<!-- Spring dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</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>
<!-- jstl for jsp page -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
</dependencies>
4.Spring MVC Web 应用程序
一个简单的控制器:
- 如果 URL =
/welcome
或/
,返回 hello 页面。 - 如果 URL =
/admin
,返回管理页面。
稍后,我们将向您展示如何使用 Spring Security 来保护带有用户登录表单的“/admin”URL。
HelloController.java
package com.mkyong.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView welcomePage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Hello World");
model.addObject("message", "This is welcome page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Hello World");
model.addObject("message", "This is protected page!");
model.setViewName("admin");
return model;
}
}
两个 JSP 页面。
hello.jsp
<%@page session="false"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
</body>
</html>
admin.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>Welcome : ${pageContext.request.userPrincipal.name}
| <a href="<c:url value="/j_spring_security_logout" />" > Logout</a></h2>
</c:if>
</body>
</html>
mvc-dispatcher-servlet.xml
<beans
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.mkyong.*" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/pages/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
</beans>
5.Spring 安全性:用户认证
创建一个 Spring 安全 XML 文件。
spring-security.xml
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<http auto-config="true">
<intercept-url pattern="/admin**" access="ROLE_USER" />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="123456" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
它告诉我们,只有用户“mkyong”被允许访问/admin
URL。
6.集成 Spring 安全
要将 Spring security 与 Spring MVC web 应用程序集成,只需将DelegatingFilterProxy
声明为一个 servlet 过滤器来拦截任何传入的请求。
web.xml
<web-app id="WebApp_ID" version="2.4"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Spring MVC Application</display-name>
<!-- Spring MVC -->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- Loads Spring Security config file -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring-security.xml
</param-value>
</context-param>
<!-- Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
7.演示
就这些,但是等等…登录表单在哪里?不用担心,如果您没有定义任何定制的登录表单,Spring 会自动创建一个简单的登录表单。
Custom Login Form
Read this “Spring Security form login example” to understand how to create a custom login form in Spring Security.
1.欢迎页面-http://localhost:8080/spring-security-helloworld-xml/welcome
2.尝试访问/admin
页面,Spring Security 会拦截请求并重定向到/spring_security_login
,会显示一个预定义的登录表单。
3.如果用户名和密码不正确,就会显示错误消息,Spring 会重定向到这个 URL /spring_security_login?login_error
。
4.如果用户名和密码正确,Spring 会将请求重定向到最初请求的 URL 并显示页面。
下载源代码
Download it – spring-security-helloworld-xml.zip (9 KB)
参考
Tags : hello world spring securityfreestar.config.enabled_slots.push({ placementName: "mkyong_leaderboard_btf", slotId: "mkyong_leaderboard_btf" });
Spring Security + Hibernate 注释示例
在本教程中,先前的Spring Security+hibernate 4 XML示例将被重用,并将其转换为基于注释的示例。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Hibernate 4.2.11 .最终版
- MySQL 服务器 5.6
- Tomcat 7 (Servlet 3.x 容器)
快速笔记:
- 用
LocalSessionFactoryBuilder
创建一个会话工厂 - 将会话工厂注入用户道
- 将 UserDao 集成到一个自定义的
UserDetailsService
中,从数据库中加载用户。
1.项目目录
最终项目目录结构。
## 2.用户模型+映射文件
模型类及其基于注释的映射文件。
User.java
package com.mkyong.users.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "users", catalog = "test")
public class User {
private String username;
private String password;
private boolean enabled;
private Set<UserRole> userRole = new HashSet<UserRole>(0);
public User() {
}
public User(String username, String password, boolean enabled) {
this.username = username;
this.password = password;
this.enabled = enabled;
}
public User(String username, String password,
boolean enabled, Set<UserRole> userRole) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.userRole = userRole;
}
@Id
@Column(name = "username", unique = true,
nullable = false, length = 45)
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
@Column(name = "password",
nullable = false, length = 60)
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
@Column(name = "enabled", nullable = false)
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
public Set<UserRole> getUserRole() {
return this.userRole;
}
public void setUserRole(Set<UserRole> userRole) {
this.userRole = userRole;
}
}
UserRole.java
package com.mkyong.users.model;
import static javax.persistence.GenerationType.IDENTITY;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
@Entity
@Table(name = "user_roles", catalog = "test",
uniqueConstraints = @UniqueConstraint(
columnNames = { "role", "username" }))
public class UserRole{
private Integer userRoleId;
private User user;
private String role;
public UserRole() {
}
public UserRole(User user, String role) {
this.user = user;
this.role = role;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "user_role_id",
unique = true, nullable = false)
public Integer getUserRoleId() {
return this.userRoleId;
}
public void setUserRoleId(Integer userRoleId) {
this.userRoleId = userRoleId;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "username", nullable = false)
public User getUser() {
return this.user;
}
public void setUser(User user) {
this.user = user;
}
@Column(name = "role", nullable = false, length = 45)
public String getRole() {
return this.role;
}
public void setRole(String role) {
this.role = role;
}
}
3.道类
DAO 类,通过 Hibernate 从数据库加载数据。
UserDao.java
package com.mkyong.users.dao;
import com.mkyong.users.model.User;
public interface UserDao {
User findByUserName(String username);
}
UserDaoImpl.java
package com.mkyong.users.dao;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.mkyong.users.model.User;
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private SessionFactory sessionFactory;
@SuppressWarnings("unchecked")
public User findByUserName(String username) {
List<User> users = new ArrayList<User>();
users = sessionFactory.getCurrentSession()
.createQuery("from User where username=?")
.setParameter(0, username)
.list();
if (users.size() > 0) {
return users.get(0);
} else {
return null;
}
}
}
4.用户详细信息服务
使用@Transactional
来声明一个事务方法。
MyUserDetailsService.java
package com.mkyong.users.service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mkyong.users.dao.UserDao;
import com.mkyong.users.model.UserRole;
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
//get user from the database, via Hibernate
@Autowired
private UserDao userDao;
@Transactional(readOnly=true)
@Override
public UserDetails loadUserByUsername(final String username)
throws UsernameNotFoundException {
com.mkyong.users.model.User user = userDao.findByUserName(username);
List<GrantedAuthority> authorities =
buildUserAuthority(user.getUserRole());
return buildUserForAuthentication(user, authorities);
}
// Converts com.mkyong.users.model.User user to
// org.springframework.security.core.userdetails.User
private User buildUserForAuthentication(com.mkyong.users.model.User user,
List<GrantedAuthority> authorities) {
return new User(user.getUsername(), user.getPassword(),
user.isEnabled(), true, true, true, authorities);
}
private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) {
Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();
// Build user's authorities
for (UserRole userRole : userRoles) {
setAuths.add(new SimpleGrantedAuthority(userRole.getRole()));
}
List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);
return Result;
}
}
5.Spring 安全注释
用注释声明和绑定一切,阅读注释,它应该是不言自明的。
SecurityConfig.java
package com.mkyong.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsService")
UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**")
.access("hasRole('ROLE_ADMIN')").and().formLogin()
.loginPage("/login").failureUrl("/login?error")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutSuccessUrl("/login?logout")
.and().csrf()
.and().exceptionHandling().accessDeniedPage("/403");
}
@Bean
public PasswordEncoder passwordEncoder(){
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
}
使用LocalSessionFactoryBuilder
创建一个会话工厂。
AppConfig.java
package com.mkyong.config;
import java.util.Properties;
import org.apache.commons.dbcp.BasicDataSource;
import org.hibernate.SessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBuilder;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration
@ComponentScan({ "com.mkyong.*" })
@EnableTransactionManagement
@Import({ SecurityConfig.class })
public class AppConfig {
@Bean
public SessionFactory sessionFactory() {
LocalSessionFactoryBuilder builder =
new LocalSessionFactoryBuilder(dataSource());
builder.scanPackages("com.mkyong.users.model")
.addProperties(getHibernateProperties());
return builder.buildSessionFactory();
}
private Properties getHibernateProperties() {
Properties prop = new Properties();
prop.put("hibernate.format_sql", "true");
prop.put("hibernate.show_sql", "true");
prop.put("hibernate.dialect",
"org.hibernate.dialect.MySQL5Dialect");
return prop;
}
@Bean(name = "dataSource")
public BasicDataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/test");
ds.setUsername("root");
return ds;
}
//Create a transaction manager
@Bean
public HibernateTransactionManager txManager() {
return new HibernateTransactionManager(sessionFactory());
}
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver
= new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/pages/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
完成了。
6.项目演示
以下视频演示为 Spring 安全数据库登录教程。由于本教程生成相同的输出,所以视频演示被重用。
//web.archive.org/web/20190220134456if_/http://www.youtube.com/embed/2ms57c2EdUg
6.1 进入密码保护页面:http://localhost:8080/spring-security-hibernate-annotation/admin,显示登录页面。
6.2 输入用户“mkyong”和密码“123456”。
6.3 使用用户“alex”和密码“123456”尝试访问/admin
页面,将显示 403 页面。
下载源代码
Download it – spring-security-hibernate-annotation.zip (35 KB)
参考
- Spring Security + Hibernate XML 示例
- 春安你好世界注释示例
- LocalSessionFactoryBuilder JavaDoc
- 春季 ORM–冬眠
- 春季冬眠 4 LocalSessionFactoryBean JavaDoc
- 春季交易管理
- Hibernate ORM 文档
- 用 JDBC 的 Spring 安全表单登录数据库
- 休眠:没有为当前线程找到会话
annotation spring hibernate spring security
Spring Security + Hibernate XML 示例
在本教程中,我们将向您展示如何在 Spring Security、XML 配置示例中集成 Hibernate 4。
Note
For annotation version, please read this Spring Security + Hibernate Annotation Example.
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- Hibernate 4.2.11 .最终版
- MySQL 服务器 5.6
- JDK 1.6
- maven3
- Eclipse 4.3
快速笔记
- 用
hibernate4.LocalSessionFactoryBean
创建一个会话工厂 - 将会话工厂注入用户道
- 为了与 Spring Security 集成,创建一个实现
UserDetailsService
接口的类,并用 UserDao 加载用户 - 必须声明事务管理器,否则 Hibernate 在 Spring 中将无法工作
1.项目目录
最终项目目录结构。
## 2.项目依赖性
POM 文件中项目依赖项的列表。
pom.xml
<properties>
<spring.version>3.2.8.RELEASE</spring.version>
<spring.security.version>3.2.3.RELEASE</spring.security.version>
<jstl.version>1.2</jstl.version>
<mysql.connector.version>5.1.30</mysql.connector.version>
<logback.version>1.1.2</logback.version>
<slf4j.version>1.7.6</slf4j.version>
<hibernate.version>4.2.11.Final</hibernate.version>
<dbcp.version>1.4</dbcp.version>
<servletapi.version>2.5</servletapi.version>
</properties>
<dependencies>
<!-- database pool -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>${dbcp.version}</version>
</dependency>
<!-- Hibernate ORM -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Spring 3 dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring + aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- ORM integration, e.g Hibernate -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</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>
<!-- Spring Security JSP Taglib -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.security.version}</version>
</dependency>
<!-- jstl for jsp page -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<!-- MySql Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<!-- logging, slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servletapi.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
3.用户表
创建存储用户和用户角色的表的 SQL 脚本。
mysql.sql
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(60) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username));
CREATE TABLE user_roles (
user_role_id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
role varchar(45) NOT NULL,
PRIMARY KEY (user_role_id),
UNIQUE KEY uni_username_role (role,username),
KEY fk_username_idx (username),
CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username));
INSERT INTO users(username,password,enabled)
VALUES ('mkyong','$2a$10$04TVADrR6/SPLBjsK0N30.Jf5fNjBugSACeGv1S69dZALR7lSov0y', true);
INSERT INTO users(username,password,enabled)
VALUES ('alex','$2a$10$04TVADrR6/SPLBjsK0N30.Jf5fNjBugSACeGv1S69dZALR7lSov0y', true);
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_USER');
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_ADMIN');
INSERT INTO user_roles (username, role)
VALUES ('alex', 'ROLE_USER');
4.用户模型+ Hibernate XML 映射
模型类及其 XML 映射文件。
User.java
package com.mkyong.users.model;
import java.util.HashSet;
import java.util.Set;
public class User {
private String username;
private String password;
private boolean enabled;
private Set<UserRole> userRole = new HashSet<UserRole>(0);
//getter and setter methods
}
UserRole.java
package com.mkyong.users.model;
public class UserRole{
private Integer userRoleId;
private User user;
private String role;
//getter and setter methods
}
Users.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.mkyong.users.model.User" table="users" catalog="test">
<id name="username" type="string">
<column name="username" length="45" />
<generator class="assigned" />
</id>
<property name="password" type="string">
<column name="password" length="60" not-null="true" />
</property>
<property name="enabled" type="boolean">
<column name="enabled" not-null="true" />
</property>
<set name="userRole" table="user_roles" inverse="true" lazy="true" fetch="select">
<key>
<column name="username" length="45" not-null="true" />
</key>
<one-to-many class="com.mkyong.users.model.UserRole" />
</set>
</class>
</hibernate-mapping>
UserRoles.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.mkyong.users.model.UserRole" table="user_roles" catalog="test">
<id name="userRoleId" type="java.lang.Integer">
<column name="user_role_id" />
<generator class="identity" />
</id>
<many-to-one name="user" class="com.mkyong.users.model.User" fetch="select">
<column name="username" length="45" not-null="true" />
</many-to-one>
<property name="role" type="string">
<column name="role" length="45" not-null="true" />
</property>
</class>
</hibernate-mapping>
5.道类
创建一个UserDao
类,通过 Hibernate 从数据库加载用户。
UserDao.java
package com.mkyong.users.dao;
import com.mkyong.users.model.User;
public interface UserDao {
User findByUserName(String username);
}
UserDaoImpl.java
package com.mkyong.users.dao;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.SessionFactory;
import com.mkyong.users.model.User;
public class UserDaoImpl implements UserDao {
private SessionFactory sessionFactory;
@SuppressWarnings("unchecked")
public User findByUserName(String username) {
List<User> users = new ArrayList<User>();
users = getSessionFactory().getCurrentSession()
.createQuery("from User where username=?")
.setParameter(0, username).list();
if (users.size() > 0) {
return users.get(0);
} else {
return null;
}
}
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}
6.用户详细信息服务
创建一个自定义的UserDetailsService
,从UserDao
加载用户,然后建立用户的权限。
Note
Example is reference from this Spring’s JdbcDaoImplMyUserDetailsService.java
package com.mkyong.users.service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.mkyong.users.dao.UserDao;
import com.mkyong.users.model.UserRole;
public class MyUserDetailsService implements UserDetailsService {
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(final String username)
throws UsernameNotFoundException {
com.mkyong.users.model.User user = userDao.findByUserName(username);
List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole());
return buildUserForAuthentication(user, authorities);
}
// Converts com.mkyong.users.model.User user to
// org.springframework.security.core.userdetails.User
private User buildUserForAuthentication(com.mkyong.users.model.User user,
List<GrantedAuthority> authorities) {
return new User(user.getUsername(),
user.getPassword(), user.isEnabled(),
true, true, true, authorities);
}
private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) {
Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();
// Build user's authorities
for (UserRole userRole : userRoles) {
setAuths.add(new SimpleGrantedAuthority(userRole.getRole()));
}
List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);
return Result;
}
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
7.Spring 安全 XML
看评论,应该不言自明。
spring-security.xml
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<!-- enable use-expressions -->
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
<!-- access denied page -->
<access-denied-handler error-page="/403" />
<form-login
login-page="/login"
default-target-url="/welcome"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password" />
<logout logout-success-url="/login?logout" />
<!-- enable csrf protection -->
<csrf />
</http>
<authentication-manager>
<authentication-provider user-service-ref="myUserDetailsService" >
<password-encoder hash="bcrypt" />
</authentication-provider>
</authentication-manager>
</beans:beans>
spring-database.xml
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- MySQL data source -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="password" />
</bean>
<!-- Hibernate session factory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
<value>orm/Users.hbm.xml</value>
<value>orm/UserRoles.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQL5Dialect
</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<bean id="userDao" class="com.mkyong.users.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="myUserDetailsService"
class="com.mkyong.users.service.MyUserDetailsService">
<property name="userDao" ref="userDao" />
</bean>
<!-- MUST have transaction manager, using aop and aspects -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"></property>
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="find*" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="userServicePointCut"
expression="execution(* com.mkyong.users.service.*Service.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="userServicePointCut" />
</aop:config>
</beans>
完成了。
Note
Those JSP, web.xml and controller files are omitted, it’s quite a standard code, if you are interested on it, please download the full source code at the end of the article.
8.项目演示
下面的视频演示是针对 Spring 安全数据库登录与 JDBC 教程的。由于本教程生成相同的输出,但是使用 Hibernate 来加载用户,所以视频演示被重用。
//web.archive.org/web/20190223081526if_/http://www.youtube.com/embed/2ms57c2EdUg
8.1 访问受密码保护的页面:http://localhost:8080/spring-security-hibernate/admin,显示登录页面。
8.2 输入用户“mkyong”和密码“123456”。
8.3 使用用户“alex”和密码“123456”尝试访问/admin
页面,将显示 403 页面。
下载源代码
Download it – spring-security-hibernate.zip (30 KB)
参考
- 春季 ORM–冬眠
- 春季冬眠 4 LocalSessionFactoryBean JavaDoc
- 春季冬眠 3 LocalSessionFactoryBean JavaDoc
- 春季交易管理
- Hibernate ORM 文档
- 用 JDBC 的 Spring 安全表单登录数据库
- 休眠:没有为当前线程找到会话
hibernate spring hibernate spring security
Spring Security HTTP 基本认证示例
配置 HTTP 基本身份验证后,web 浏览器将显示用户身份验证的登录对话框。本教程向您展示了如何在 Spring Security 中配置 HTTP 基本身份验证。
<http>
<intercept-url pattern="/welcome*" access="ROLE_USER" />
<http-basic />
</http>
最后一个 Spring Security 基于表单的登录示例将被重用,但是切换身份验证以支持 HTTP basic。
1.春天安全
要启用 HTTP basic,只需将“ form-login ”更改为“ http-basic ”标签。
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">
<!-- HTTP basic authentication in Spring Security -->
<http>
<intercept-url pattern="/welcome*" access="ROLE_USER" />
<http-basic />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="123456" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
完了,就这样。
2.演示
当访问安全的 URL 时,浏览器会自动显示一个登录对话框。
网址:http://localhost:8080/spring MVC/welcome
## 下载源代码
Download it – Spring-Security-HTTP-Basic-Authentication-Example.zip (9 KB)
参考
authentication spring security
Spring 安全性:限制登录尝试示例
在本教程中,我们将向您展示如何在 Spring Security 中限制登录尝试,这意味着,如果用户尝试使用无效密码登录超过 3 次,系统将锁定该用户,使其无法再登录。
使用的技术和工具:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- 春季 JDBC 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
- MySQL 服务器 5.6
- Tomcat 7 (Servlet 3.x)
本教程的一些快速注释:
- 将使用 MySQL 数据库。
- 这是一个基于 Spring 安全注释的例子。
- 创建一个包含“accountNonLocked”列的“users”表。
- 创建一个“user_attempts”表来存储无效的登录尝试。
- 将使用弹簧 JDBC。
- 根据返回的异常显示自定义错误信息。
- 创建自定义的“authenticationProvider”
1.解决办法
查看现有的 Spring Security 的身份验证类,已经实现了“锁定”特性。要启用限制登录尝试,您需要将UserDetails.isAccountNonLocked
设置为 false。
DaoAuthenticationProvider.java
package org.springframework.security.authentication.dao;
public class DaoAuthenticationProvider
extends AbstractUserDetailsAuthenticationProvider {
//...
}
AbstractUserDetailsAuthenticationProvider.java
package org.springframework.security.authentication.dao;
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean,
MessageSourceAware {
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
logger.debug("User account is locked");
throw new LockedException(
messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"), user);
}
//...
}
}
2.项目演示
//web.archive.org/web/20190210095107if_/http://www.youtube.com/embed/vyoD4ALC43s
3.项目目录
查看最终项目结构(基于注释):
4.数据库ˌ资料库
下面是创建 users、user_roles 和 user_attempts 表的 MySQL 脚本。
4.1 创建一个“用户”表,列为“accountNonLocked”。
users.sql
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(45) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
accountNonExpired TINYINT NOT NULL DEFAULT 1 ,
accountNonLocked TINYINT NOT NULL DEFAULT 1 ,
credentialsNonExpired TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (username));
4.2 创建一个“用户角色”表。
user_roles.sql
CREATE TABLE user_roles (
user_role_id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
role varchar(45) NOT NULL,
PRIMARY KEY (user_role_id),
UNIQUE KEY uni_username_role (role,username),
KEY fk_username_idx (username),
CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username));
4.3 创建“用户尝试次数”表。
user_attempts.sql
CREATE TABLE user_attempts (
id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
attempts varchar(45) NOT NULL,
lastModified datetime NOT NULL,
PRIMARY KEY (id)
);
4.4 插入一个用户进行测试。
INSERT INTO users(username,password,enabled)
VALUES ('mkyong','123456', true);
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_USER');
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_ADMIN');
5.用户尝试类别
该类表示“用户尝试次数”表的数据。
UserAttempts.java
package com.mkyong.users.model;
import java.util.Date;
public class UserAttempts {
private int id;
private String username;
private int attempts;
private Date lastModified;
//getter and setter
}
6.道类
一个 DAO 类,用来更新无效的登录尝试,阅读注释,不言自明。
UserDetailsDao.java
package com.mkyong.users.dao;
import com.mkyong.users.model.UserAttempts;
public interface UserDetailsDao {
void updateFailAttempts(String username);
void resetFailAttempts(String username);
UserAttempts getUserAttempts(String username);
}
UserDetailsDaoImpl.java
package com.mkyong.users.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Repository;
import com.mkyong.users.model.UserAttempts;
@Repository
public class UserDetailsDaoImpl extends JdbcDaoSupport implements UserDetailsDao {
private static final String SQL_USERS_UPDATE_LOCKED = "UPDATE USERS SET accountNonLocked = ? WHERE username = ?";
private static final String SQL_USERS_COUNT = "SELECT count(*) FROM USERS WHERE username = ?";
private static final String SQL_USER_ATTEMPTS_GET = "SELECT * FROM USER_ATTEMPTS WHERE username = ?";
private static final String SQL_USER_ATTEMPTS_INSERT = "INSERT INTO USER_ATTEMPTS (USERNAME, ATTEMPTS, LASTMODIFIED) VALUES(?,?,?)";
private static final String SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = attempts + 1, lastmodified = ? WHERE username = ?";
private static final String SQL_USER_ATTEMPTS_RESET_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = 0, lastmodified = null WHERE username = ?";
private static final int MAX_ATTEMPTS = 3;
@Autowired
private DataSource dataSource;
@PostConstruct
private void initialize() {
setDataSource(dataSource);
}
@Override
public void updateFailAttempts(String username) {
UserAttempts user = getUserAttempts(username);
if (user == null) {
if (isUserExists(username)) {
// if no record, insert a new
getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT, new Object[] { username, 1, new Date() });
}
} else {
if (isUserExists(username)) {
// update attempts count, +1
getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS, new Object[] { new Date(), username});
}
if (user.getAttempts() + 1 >= MAX_ATTEMPTS) {
// locked user
getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED, new Object[] { false, username });
// throw exception
throw new LockedException("User Account is locked!");
}
}
}
@Override
public UserAttempts getUserAttempts(String username) {
try {
UserAttempts userAttempts = getJdbcTemplate().queryForObject(SQL_USER_ATTEMPTS_GET,
new Object[] { username }, new RowMapper<UserAttempts>() {
public UserAttempts mapRow(ResultSet rs, int rowNum) throws SQLException {
UserAttempts user = new UserAttempts();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setAttempts(rs.getInt("attempts"));
user.setLastModified(rs.getDate("lastModified"));
return user;
}
});
return userAttempts;
} catch (EmptyResultDataAccessException e) {
return null;
}
}
@Override
public void resetFailAttempts(String username) {
getJdbcTemplate().update(
SQL_USER_ATTEMPTS_RESET_ATTEMPTS, new Object[] { username });
}
private boolean isUserExists(String username) {
boolean result = false;
int count = getJdbcTemplate().queryForObject(
SQL_USERS_COUNT, new Object[] { username }, Integer.class);
if (count > 0) {
result = true;
}
return result;
}
}
7.用户详细信息服务
默认情况下,JdbcDaoImpl
会一直将accountNonLocked
设置为 true,这不是我们想要的。查看源代码。
JdbcDaoImpl.java
package org.springframework.security.core.userdetails.jdbc;
public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
//...
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
}
});
}
为了节省开发时间,您可以扩展JdbcDaoImpl
并覆盖loadUsersByUsername
和createUserDetails
来获得定制的UserDetails
。
CustomUserDetailsService.java
package com.mkyong.users.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.stereotype.Service;
/**
* Reference org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
*
* @author mkyong
*
*/
@Service("userDetailsService")
public class CustomUserDetailsService extends JdbcDaoImpl {
@Autowired
private DataSource dataSource;
@PostConstruct
private void initialize() {
setDataSource(dataSource);
}
@Override
@Value("select * from users where username = ?")
public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
super.setUsersByUsernameQuery(usersByUsernameQueryString);
}
@Override
@Value("select username, role from user_roles where username =?")
public void setAuthoritiesByUsernameQuery(String queryString) {
super.setAuthoritiesByUsernameQuery(queryString);
}
//override to get accountNonLocked
@Override
public List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new String[] { username },
new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString("username");
String password = rs.getString("password");
boolean enabled = rs.getBoolean("enabled");
boolean accountNonExpired = rs.getBoolean("accountNonExpired");
boolean credentialsNonExpired = rs.getBoolean("credentialsNonExpired");
boolean accountNonLocked = rs.getBoolean("accountNonLocked");
return new User(username, password, enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
}
});
}
//override to pass accountNonLocked
@Override
public UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
List<GrantedAuthority> combinedAuthorities) {
String returnUsername = userFromUserQuery.getUsername();
if (super.isUsernameBasedPrimaryKey()) {
returnUsername = username;
}
return new User(returnUsername, userFromUserQuery.getPassword(),
userFromUserQuery.isEnabled(),
userFromUserQuery.isAccountNonExpired(),
userFromUserQuery.isCredentialsNonExpired(),
userFromUserQuery.isAccountNonLocked(), combinedAuthorities);
}
}
8.DaoAuthenticationProvider
创建一个自定义身份验证提供程序,对于每次无效的登录尝试,更新 user_attempts 表,如果达到最大尝试次数,则抛出LockedException
。
LimitLoginAuthenticationProvider.java
package com.mkyong.web.handler;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import com.mkyong.users.dao.UserDetailsDao;
import com.mkyong.users.model.UserAttempts;
@Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider {
@Autowired
UserDetailsDao userDetailsDao;
@Autowired
@Qualifier("userDetailsService")
@Override
public void setUserDetailsService(UserDetailsService userDetailsService) {
super.setUserDetailsService(userDetailsService);
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
try {
Authentication auth = super.authenticate(authentication);
//if reach here, means login success, else an exception will be thrown
//reset the user_attempts
userDetailsDao.resetFailAttempts(authentication.getName());
return auth;
} catch (BadCredentialsException e) {
//invalid login, update to user_attempts
userDetailsDao.updateFailAttempts(authentication.getName());
throw e;
} catch (LockedException e){
//this user is locked!
String error = "";
UserAttempts userAttempts =
userDetailsDao.getUserAttempts(authentication.getName());
if(userAttempts!=null){
Date lastAttempts = userAttempts.getLastModified();
error = "User account is locked! <br><br>Username : "
+ authentication.getName() + "<br>Last Attempts : " + lastAttempts;
}else{
error = e.getMessage();
}
throw new LockedException(error);
}
}
}
9.弹簧控制器
一个标准的控制器类,参考login
方法,它向你展示了如何摆弄会话值——“SPRING _ SECURITY _ LAST _ EXCEPTION”,并自定义错误消息。
MainController.java
package com.mkyong.web.controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class MainController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView defaultPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Limit Login - Annotation");
model.addObject("message", "This is default page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Limit Login - Annotation");
model.addObject("message", "This page is for ROLE_ADMIN only!");
model.setViewName("admin");
return model;
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout,
HttpServletRequest request) {
ModelAndView model = new ModelAndView();
if (error != null) {
model.addObject("error",
getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION"));
}
if (logout != null) {
model.addObject("msg", "You've been logged out successfully.");
}
model.setViewName("login");
return model;
}
//customize the error message
private String getErrorMessage(HttpServletRequest request, String key){
Exception exception =
(Exception) request.getSession().getAttribute(key);
String error = "";
if (exception instanceof BadCredentialsException) {
error = "Invalid username and password!";
}else if(exception instanceof LockedException) {
error = exception.getMessage();
}else{
error = "Invalid username and password!";
}
return error;
}
// for 403 access denied page
@RequestMapping(value = "/403", method = RequestMethod.GET)
public ModelAndView accesssDenied() {
ModelAndView model = new ModelAndView();
// check if user is login
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!(auth instanceof AnonymousAuthenticationToken)) {
UserDetails userDetail = (UserDetails) auth.getPrincipal();
System.out.println(userDetail);
model.addObject("username", userDetail.getUsername());
}
model.setViewName("403");
return model;
}
}
10.Spring 安全配置
附上您定制的authenticationProvider
。
SecurityConfig.java
package com.mkyong.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("authenticationProvider")
AuthenticationProvider authenticationProvider;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/admin/**")
.access("hasRole('ROLE_USER')").and().formLogin()
.loginPage("/login").failureUrl("/login?error")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutSuccessUrl("/login?logout").and().csrf();
}
}
完成了。
11.演示
演示页面–http://localhost:8080/spring-security-limit-log in-annotation/admin
11.1,第一次无效登录尝试,将显示一个正常的错误消息。
11.2,如果达到无效登录尝试的最大次数,将显示错误消息“用户帐户被锁定”。
11.3、如果用户处于“锁定”状态,仍然尝试再次登录。将显示锁定的详细信息。
11.4 查看“用户”表,如果“account non locked”= 0 或 false,则表示该用户处于锁定状态。
下载源代码
Download it – spring-security-limit-login-annotation.zip (38 KB)Download it – spring-security-limit-login-xml.zip (32 KB)
参考
- StackOverflow:如何在 Spring Security 中限制登录尝试?
- Spring 官方参考:核心服务-authenticationProvider
- Spring Security 自定义登录表单注释示例
- Spring DaoAuthenticationProvider JavaDoc
- Spring JdbcDaoImpl JavaDoc
Spring 安全注销示例
在 Spring Security 中,要注销,只需添加一个链接到 URL“j _ Spring _ Security _ logout”,例如:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<h2>messages, whatever</h2>
<a href="<c:url value="j_spring_security_logout" />" > Logout</a>
</body>
</html>
在 Spring security 中,声明“logout
”标签,并配置“logout-success-url
”属性:
<beans:beans
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">
<http auto-config="true">
<intercept-url pattern="/welcome*" access="ROLE_USER" />
<logout logout-success-url="/welcome" />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="mkyong" password="password" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
下载源代码
Download it – Spring-Security-LogOut-Example.zip (8 KB)logout spring security (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190301115812/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Spring 安全密码哈希示例
在本教程中,我们将向您展示如何使用BCryptPasswordEncoder
散列密码并在 Spring Security 中执行登录验证。
在过去,通常情况下,我们使用 MD5 Md5PasswordEncoder
或 SHA ShaPasswordEncoder
哈希算法对密码进行编码……您仍然可以使用任何您喜欢的编码器,但 Spring 建议使用 BCrypt BCryptPasswordEncoder
,这是一种更强的哈希算法,带有随机生成的 salt。
使用的技术:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- 春季 JDBC 3.2.3 .发布
- MySQL 服务器 5.6
1.查看密码编码
熟悉的旧认证PasswordEncoder
界面已被弃用…
package org.springframework.security.authentication.encoding;
//Implementation : Md5PasswordEncoder and ShaPasswordEncoder
@Deprecated
public interface PasswordEncoder {
相反,你应该使用这个新的加密接口。
package org.springframework.security.crypto.password;
//Implementation : BCryptPasswordEncoder
public interface PasswordEncoder {
2.生成 BCrypt 密码
首先,散列一个密码,并将其放入数据库中,以便以后进行登录验证。这个例子使用BCryptPasswordEncoder
散列一个密码“123456”。
PasswordEncoderGenerator.java
package com.mkyong.web.controller;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordEncoderGenerator {
public static void main(String[] args) {
int i = 0;
while (i < 10) {
String password = "123456";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String hashedPassword = passwordEncoder.encode(password);
System.out.println(hashedPassword);
i++;
}
}
}
在 BCrypt 哈希算法中,每次都会生成长度为 60 的不同哈希值。
$2a$10$EblZqNptyYvcLm/VwDCVAuBjzZOI7khzdyGPBr08PpIi0na624b8.
$2a$10$trT3.R/Nfey62eczbKEnueTcIbJXW.u1ffAo/XfyLpofwNDbEB86O
$2a$10$teJrCEnsxNT49ZpXU7n22O27aCGbVYYe/RG6/XxdWPJbOLZubLIi2
$2a$10$BHG59UT6p7bgT6U2fQ/9wOyTIdejh4Rk1vWilvl4b6ysNPdhnViUS
$2a$10$W9oRWeFmOT0bByL5fmAceucetmEYFg2yzq3e50mcu.CO7rUDb/poG
$2a$10$HApapHvDStTEwjjneMCvxuqUKVyycXZRfXMwjU0rRmaWMsjWQp/Zu
$2a$10$GYCkBzp2NlpGS/qjp5f6NOWHeF56ENAlHNuSssSJpE1MMYJevHBWO
$2a$10$gwbTCaIR/qE1uYhvEY6GG.bNDQcZuYQX9tkVwaK/aD7ZLPptC.7QC
$2a$10$5uKS72xK2ArGDgb2CwjYnOzQcOmB7CPxK6fz2MGcDBM9vJ4rUql36
$2a$10$6TajU85/gVrGUm5fv5Z8beVF37rlENohyLk3BEpZJFi6Av9JNkw9O
每次用 BCrypt 散列一个值都会得到不同的值,这很正常,因为 salt 是随机生成的。在本教程中,我们获得第一个输出,并将其插入数据库。
3.数据库ˌ资料库
创建表并插入用户“mkyong”进行测试。
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(60) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username));
CREATE TABLE user_roles (
user_role_id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
role varchar(45) NOT NULL,
PRIMARY KEY (user_role_id),
UNIQUE KEY uni_username_role (role,username),
KEY fk_username_idx (username),
CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username));
INSERT INTO users(username,password,enabled)
VALUES ('mkyong','$2a$10$EblZqNptyYvcLm/VwDCVAuBjzZOI7khzdyGPBr08PpIi0na624b8.', true);
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_USER');
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_ADMIN');
4.启用密码编码器
在 XML 配置中启用密码编码器的几种方法。
4.1 使用默认的 BCryptPasswordEncoder 。
spring-security.xml
<authentication-manager>
<authentication-provider>
<password-encoder hash="bcrypt" />
</authentication-provider>
</authentication-manager>
4.2 向 BCryptPasswordEncoder 传递一个“强度”参数。
spring-security.xml
<authentication-manager>
<authentication-provider>
<password-encoder ref="encoder" />
</authentication-provider>
</authentication-manager>
<beans:bean id="encoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
<beans:constructor-arg name="strength" value="11" />
</beans:bean>
4.3 将编码器传递给DaoAuthenticationProvider。
spring-security.xml
<bean id="authProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="customUserService" />
<property name="passwordEncoder" ref="encoder" />
</bean>
<bean id="encoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
4.4 注释示例。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
public void configAuthentication(AuthenticationManagerBuilder auth)
throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.passwordEncoder(passwordEncoder())
.usersByUsernameQuery("sql...")
.authoritiesByUsernameQuery("sql...");
}
@Bean
public PasswordEncoder passwordEncoder(){
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
5.项目演示
访问密码保护页面:localhost:8080/spring-security-password-hashing/admin,显示登录页面。输入密码“123456”,Spring Security 将对密码进行哈希处理,并与数据库中的哈希密码进行比较。
数据库中的用户和密码。
下载源代码
Download – spring-security-password-hashing.zip(18 KB)Download – spring-security-password-hashing-annotation.zip (22 KB)
参考
Spring Security 记住我示例
在本教程中,我们将向您展示如何在 Spring Security 中实现“记住我”登录功能,这意味着,即使在用户的会话过期后,系统也会记住用户并执行自动登录。
使用的技术和工具:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
- 春季 JDBC 3.2.3 .发布
- Eclipse 4.2
- JDK 1.6
- maven3
- MySQL 服务器 5.6
- Tomcat 6 和 7 (Servlet 3.x)
- 用谷歌浏览器测试
一些快速注释:
- 在 Spring Security 中,有两种实现“记住我”的方法——简单的基于哈希的令牌和持久令牌方法。
- 要了解“记住我”是如何工作的,请阅读这些文章-Spring remember me reference、持久登录 Cookie 最佳实践、改进的持久登录 Cookie 最佳实践。
- 这个例子使用的是“持久令牌方法”,参考 Spring 的
PersistentTokenBasedRememberMeServices
。 - 这个例子使用了 MySQL 和数据库认证(通过 Spring JDBC)。
- 将创建表“persistent_logins”来存储登录令牌和序列。
项目工作流:
- 如果用户登录时勾选了“记住我”,系统将在请求的浏览器中存储一个“记住我”cookie。
- 如果用户的浏览器提供了有效的“记住我”cookie,系统将执行自动登录。
- 如果用户通过“记住我”cookie 登录,要更新用户详细信息,用户需要再次键入用户名和密码(这是一种避免窃取 cookie 来更新用户信息的良好做法。
这是“记住我”应该如何工作的一个非常高的水平,详情请参考“快速笔记”中的以上链接。
1.项目演示
//web.archive.org/web/20190225101058if_/http://www.youtube.com/embed/HUnGc4WKgRk
2.项目目录
查看项目目录结构。
## 3.MySQL 脚本
创建users
、user_roles
和persistent_logins
的 SQL 脚本。
CREATE TABLE users (
username VARCHAR(45) NOT NULL ,
password VARCHAR(45) NOT NULL ,
enabled TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username));
CREATE TABLE user_roles (
user_role_id int(11) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL,
role varchar(45) NOT NULL,
PRIMARY KEY (user_role_id),
UNIQUE KEY uni_username_role (role,username),
KEY fk_username_idx (username),
CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username));
INSERT INTO users(username,password,enabled)
VALUES ('mkyong','123456', true);
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_USER');
INSERT INTO user_roles (username, role)
VALUES ('mkyong', 'ROLE_ADMIN');
CREATE TABLE persistent_logins (
username varchar(64) not null,
series varchar(64) not null,
token varchar(64) not null,
last_used timestamp not null,
PRIMARY KEY (series)
);
4.记住我(XML 示例)
为了在 XML 配置中启用“记住我”,将remember-me
标记放在http
中,如下所示:
spring-security.xml
<!-- enable use-expressions -->
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
<form-login login-page="/login"
default-target-url="/welcome"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password"
login-processing-url="/auth/login_check"
authentication-success-handler-ref="savedRequestAwareAuthenticationSuccessHandler" />
<logout logout-success-url="/login?logout" delete-cookies="JSESSIONID" />
<csrf />
<!-- enable remember me -->
<remember-me
token-validity-seconds="1209600"
remember-me-parameter="remember-me"
data-source-ref="dataSource" />
</http>
spring-database.xml
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="password" />
</bean>
<!-- If request parameter "targetUrl" is existed, then forward to this url -->
<!-- For update login form -->
<bean id="savedRequestAwareAuthenticationSuccessHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="targetUrlParameter" value="targetUrl" />
</bean>
- token-validity-seconds—“勿忘我”cookie 的过期日期,以秒为单位。例如,1209600 = 2 周(14 天),86400 = 1 天,18000 = 5 小时。
- 记住我的参数–“复选框”的名称。默认为' _spring_security_remember_me '。
- data-source-ref–如果指定,将使用“持久令牌方法”。默认为“简单的基于散列的令牌方法”。
5.记住我(注释示例)
相当于注释:
SecurityConfig.java
package com.mkyong.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
//...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.and()
.formLogin()
.successHandler(savedRequestAwareAuthenticationSuccessHandler())
.loginPage("/login")
.failureUrl("/login?error")
.loginProcessingUrl("/auth/login_check")
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout().logoutSuccessUrl("/login?logout")
.and()
.csrf()
.and()
.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600);
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
@Bean
public SavedRequestAwareAuthenticationSuccessHandler
savedRequestAwareAuthenticationSuccessHandler() {
SavedRequestAwareAuthenticationSuccessHandler auth
= new SavedRequestAwareAuthenticationSuccessHandler();
auth.setTargetUrlParameter("targetUrl");
return auth;
}
}
P.S 在批注配置中,“记住我”复选框的默认 http 名称是“记住我”。
6.HTML/JSP 页面
6.1 在 JSP 中,您可以使用 Spring 安全标签sec:authorize access="isRememberMe()"
来确定该用户是否通过“记住我”cookies 登录。
admin.jsp
<%@taglib prefix="sec"
uri="http://www.springframework.org/security/tags"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>
<c:url value="/j_spring_security_logout" var="logoutUrl" />
<form action="${logoutUrl}" method="post" id="logoutForm">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
<script>
function formSubmit() {
document.getElementById("logoutForm").submit();
}
</script>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
Welcome : ${pageContext.request.userPrincipal.name} | <a
href="javascript:formSubmit()"> Logout</a>
</h2>
</c:if>
<sec:authorize access="isRememberMe()">
<h2># This user is login by "Remember Me Cookies".</h2>
</sec:authorize>
<sec:authorize access="isFullyAuthenticated()">
<h2># This user is login by username / password.</h2>
</sec:authorize>
</body>
</html>
6.2 带有“记住我”复选框的简单登录表单。
login.jsp
<form name='loginForm'
action="<c:url value='/auth/login_check?targetUrl=${targetUrl}' />"
method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username'></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<!-- if this is login for update, ignore remember me check -->
<c:if test="${empty loginUpdate}">
<tr>
<td></td>
<td>Remember Me: <input type="checkbox" name="remember-me" /></td>
</tr>
</c:if>
<tr>
<td colspan='2'><input name="submit" type="submit"
value="submit" /></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
6.3 更新页面。只有使用密码登录的用户才能访问此页面。
update.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@page session="true"%>
<html>
<body>
<h1>Title : Spring Security Remember Me Example - Update Form</h1>
<h1>Message : This page is for ROLE_ADMIN and fully authenticated only
(Remember me cookie is not allowed!)</h1>
<h2>Update Account Information...</h2>
</body>
</html>
7.控制器
Spring 控制器类,阅读注释不言自明。
MainController.java
package com.mkyong.web.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class MainController {
@RequestMapping(value = { "/", "/welcome**" }, method = RequestMethod.GET)
public ModelAndView defaultPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Remember Me");
model.addObject("message", "This is default page!");
model.setViewName("hello");
return model;
}
@RequestMapping(value = "/admin**", method = RequestMethod.GET)
public ModelAndView adminPage() {
ModelAndView model = new ModelAndView();
model.addObject("title", "Spring Security Remember Me");
model.addObject("message", "This page is for ROLE_ADMIN only!");
model.setViewName("admin");
return model;
}
/**
* This update page is for user login with password only.
* If user is login via remember me cookie, send login to ask for password again.
* To avoid stolen remember me cookie to update info
*/
@RequestMapping(value = "/admin/update**", method = RequestMethod.GET)
public ModelAndView updatePage(HttpServletRequest request) {
ModelAndView model = new ModelAndView();
if (isRememberMeAuthenticated()) {
//send login for update
setRememberMeTargetUrlToSession(request);
model.addObject("loginUpdate", true);
model.setViewName("/login");
} else {
model.setViewName("update");
}
return model;
}
/**
* both "normal login" and "login for update" shared this form.
*
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout,
HttpServletRequest request) {
ModelAndView model = new ModelAndView();
if (error != null) {
model.addObject("error", "Invalid username and password!");
//login form for update page
//if login error, get the targetUrl from session again.
String targetUrl = getRememberMeTargetUrlFromSession(request);
System.out.println(targetUrl);
if(StringUtils.hasText(targetUrl)){
model.addObject("targetUrl", targetUrl);
model.addObject("loginUpdate", true);
}
}
if (logout != null) {
model.addObject("msg", "You've been logged out successfully.");
}
model.setViewName("login");
return model;
}
/**
* Check if user is login by remember me cookie, refer
* org.springframework.security.authentication.AuthenticationTrustResolverImpl
*/
private boolean isRememberMeAuthenticated() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass());
}
/**
* save targetURL in session
*/
private void setRememberMeTargetUrlToSession(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session!=null){
session.setAttribute("targetUrl", "/admin/update");
}
}
/**
* get targetURL from session
*/
private String getRememberMeTargetUrlFromSession(HttpServletRequest request){
String targetUrl = "";
HttpSession session = request.getSession(false);
if(session!=null){
targetUrl = session.getAttribute("targetUrl")==null?""
:session.getAttribute("targetUrl").toString();
}
return targetUrl;
}
}
8.演示
8.1 访问受保护页面-http://localhost:8080/spring-security-remember-me/admin,系统会将用户重定向到登录表单。尝试在选中“记住我”的情况下登录。
8.2 在 Google Chrome 中,设置->显示高级设置->隐私、内容设置… ->“所有 cookie 和站点数据”——有两个 cookie 用于本地主机,一个用于当前会话,一个用于“记住我”登录 cookie。
8.3 审查表“持久登录”,用户名、系列和令牌已存储。
8.4 重启 web 应用,去 Chrome“所有 cookies 和站点数据”,移除浏览器的 session“JSESSIONID”。再次尝试访问登录页面。现在,系统将“记住你”,并通过浏览器中的登录 cookies 自动登录。
8.5 尝试访问“更新”页面-http://localhost:8080/spring-security-remember-me/admin/update,如果用户通过“记住我”cookies 登录,系统会将用户重新重定向到登录表单。这是一个很好的做法,可以避免通过窃取 cookie 来更新用户详细信息。
8.6 搞定。
9.混杂的
要学习的一些重要的 Spring 安全类:
- org . spring framework . security . config . annotation . web . configurers . remember me configurer . Java
- org . spring framework . security . web . authentic ation . remember me . abstractrememberservices . Java
- org . spring framework . security . web . authentic ation . remember me . persistenttokenbasedrememberservices . Java
- org . spring framework . security . web . authentic ation . remember me . tokenbasedremembermeservices . Java
- org . spring framework . security . web . authentic ation . remember me . remembermeauthenticationfilter
下载源代码
Download it – spring-security-remember-me.zip (18 KB)Download it – spring-security-remember-me-annotation.zip (25 KB)
参考
- 春安记得我参考资料
- 持久登录 Cookie 最佳实践
- 改进的持久登录 Cookie 最佳实践
- 对于一个网站来说,实现“记住我”的最好方法是什么?
- Spring JDBC template JavaDoc
- 使用数据库的 Spring 安全表单登录–XML 和注释示例
login form remember me spring security
spring Security–没有为 id“null”映射的 PasswordEncoder
发送带有用户名和密码的 GET 请求,但遇到密码编码器错误?
测验
- Spring Boot 2.1.2 .版本
- 发布
$ curl localhost:8080/books -u user:password
{
"timestamp":"2019-02-22T15:03:49.322+0000",
"status":500,
"error":"Internal Server Error",
"message":"There is no PasswordEncoder mapped for the id \"null\"",
"path":"/books"
}
日志中的错误
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
下面是配置。
SpringSecurityConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
.and()
.withUser("admin").password("password").roles("ADMIN");
}
}
解决办法
在 Spring Security 5.0 之前,默认的PasswordEncoder
是NoOpPasswordEncoder
,需要明文密码。在 Spring Security 5 中,默认为DelegatingPasswordEncoder
,需要密码存储格式。
解决方案 1–添加密码存储格式,对于纯文本,添加{noop}
SpringSecurityConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}password").roles("ADMIN");
}
}
解决方案二–UserDetailsService
的User.withDefaultPasswordEncoder()
SpringSecurityConfig.java
package com.mkyong.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("USER", "ADMIN").build());
return manager;
}
}
参考
春季安全教程
原文:http://web.archive.org/web/20230101150211/https://mkyong.com/tutorials/spring-security-tutorials/
Spring Security 是一个灵活而强大的认证和访问控制框架,用于保护基于 Spring 的 Java web 应用程序。
本教程中使用的 Spring 版本:
- 弹簧 3.2.8 释放
- Spring Security 3.2.3 .发布
Note
Try this Spring Boot + Spring Security + Thymeleaf example
1.Spring 安全示例
向您展示如何使用 Spring Security 保护您的 web 应用程序的示例。
- Spring Security Hello World XML 示例
Spring MVC+Spring Security 基于 XML 的项目,使用默认登录表单。 - Spring Security Hello World 注释示例
Spring MVC+Spring Security 基于注释的项目,使用默认登录表单。 - Spring Security 自定义登录表单 XML 示例
Spring MVC+Spring Security 基于 XML 的项目,自定义登录表单,注销功能,CSRF 保护和内存认证。 - Spring Security 自定义登录表单注释示例
Spring MVC+Spring Security 基于注释的项目,自定义登录表单,注销功能,CSRF 保护和内存认证。 - 使用数据库的 Spring 安全表单登录–XML 和注释示例
数据库认证、Spring 安全、JSP taglibs、JDBC、定制 403 拒绝访问页面等,XML 和注释均有。 - Spring Security:限制登录尝试——XML 和注释示例
如果用户尝试了 3 次无效登录,则锁定用户帐户。 - 春安记我的例子 【T2 记我的“持久令牌方法”的例子。额外:来自“记住我”cookie 的用户登录无法执行更新操作。
- Spring Security 密码哈希示例
密码编码器采用 BCrypt 算法。 - Spring Security + Hibernate XML 示例
使用 Hibernate 加载用户进行数据库认证。 - Spring Security + Hibernate 注释示例
使用 Hibernate 加载用户进行数据库认证。
2.常见问题
春季安全中的一些常见问题。
- 定制 403 拒绝访问页面
像主题一样,向你展示了如何定制一个 403 拒绝访问页面,使用错误页面属性和定制处理程序。 - 检查用户是否来自“记住我”cookie
如果认证= = RememberMeAuthenticationToken - 编码的密码看起来不像 BCrypt
“密码”的长度不足以存储 BCrypt 的散列值。 - 如何在 Spring Security 中获取当前登录用户名
在 Spring Security 中获取当前登录用户名的 3 种方法。 - ClassNotFoundException:org . spring framework . security . web . saved request . defaultsavedrequest
一些过时或废弃的文章…可能会在未来更新。
- Spring Security HTTP 基本认证示例
浏览器显示登录对话框进行认证。 - Spring 安全访问控制示例
在 web 应用上实现访问控制或授权的示例。 - 在 Spring Security 中显示自定义错误消息
如何轻松覆盖默认的 Spring Security 错误消息。 - Spring Security 注销示例
简单的示例向您展示如何实现注销功能。
参考
spring–使用 MailSender 通过 Gmail SMTP 服务器发送电子邮件
Spring 附带了一个有用的'org . spring framework . mail . JavaMail . javamailsenderimpl'类来简化通过 JavaMail API 发送电子邮件的过程。这里有一个 Maven 构建项目,使用 Spring 的“ JavaMailSenderImpl 通过 Gmail SMTP 服务器发送电子邮件。
1.项目依赖性
添加 JavaMail 和 Spring 的依赖项。
文件:pom.xml
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mkyong.common</groupId>
<artifactId>SpringExample</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>SpringExample</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<id>Java.Net</id>
<url>http://download.java.net/maven/2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- Java Mail API -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.3</version>
</dependency>
<!-- Spring framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6</version>
</dependency>
</dependencies>
</project>
2.春天的邮件发送者
用 Spring 的 MailSender 接口发送电子邮件的 Java 类。
文件:MailMail.java
package com.mkyong.common;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
public class MailMail
{
private MailSender mailSender;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void sendMail(String from, String to, String subject, String msg) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(msg);
mailSender.send(message);
}
}
3.Bean 配置文件
配置 mailSender bean 并指定 Gmail SMTP 服务器的电子邮件详细信息。
Note
Gmail configuration details – http://mail.google.com/support/bin/answer.py?hl=en&answer=13287
文件:Spring-Mail.xml
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="username" />
<property name="password" value="password" />
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
</props>
</property>
</bean>
<bean id="mailMail" class="com.mkyong.common.MailMail">
<property name="mailSender" ref="mailSender" />
</bean>
</beans>
4.运行它
package com.mkyong.common;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Spring-Mail.xml");
MailMail mm = (MailMail) context.getBean("mailMail");
mm.sendMail("from@no-spam.com",
"to@no-spam.com",
"Testing123",
"Testing only \n\n Hello Spring Email Sender");
}
}
下载源代码
Download it – Spring-Email-Gmail-Smtp-Example.zipTags : email gmail smtp spring
spring–发送带附件的电子邮件
这里有一个使用 Spring 通过 Gmail SMTP 服务器发送带有附件的电子邮件的例子。为了在你的电子邮件中包含附件,你必须使用 Spring 的Java mail sender&mime message,而不是mail sender&simple mail message。
1.项目依赖性
添加 JavaMail 和 Spring 的依赖项。
文件:pom.xml
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mkyong.common</groupId>
<artifactId>SpringExample</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>SpringExample</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<id>Java.Net</id>
<url>http://download.java.net/maven/2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- Java Mail API -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.3</version>
</dependency>
<!-- Spring framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6</version>
</dependency>
</dependencies>
</project>
freestar.config.enabled_slots.push({ placementName: "mkyong_incontent_1", slotId: "mkyong_incontent_1" });
2.春天的邮件发送者
你要用 JavaMailSender 代替 MailSender 发送附件,用 MimeMessageHelper 附加资源。在本例中,它将从您的文件系统(FileSystemResource)中获取“c:\log.txt”文本文件作为电子邮件附件。
除了文件系统,你还可以从 URL path( UrlResource )、class path(class path resource)、InputStream(InputStream resource)……请参考 Spring 的 AbstractResource 实现类。
文件:MailMail.java
package com.mkyong.common;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.MailParseException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
public class MailMail
{
private JavaMailSender mailSender;
private SimpleMailMessage simpleMailMessage;
public void setSimpleMailMessage(SimpleMailMessage simpleMailMessage) {
this.simpleMailMessage = simpleMailMessage;
}
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendMail(String dear, String content) {
MimeMessage message = mailSender.createMimeMessage();
try{
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(simpleMailMessage.getFrom());
helper.setTo(simpleMailMessage.getTo());
helper.setSubject(simpleMailMessage.getSubject());
helper.setText(String.format(
simpleMailMessage.getText(), dear, content));
FileSystemResource file = new FileSystemResource("C:\\log.txt");
helper.addAttachment(file.getFilename(), file);
}catch (MessagingException e) {
throw new MailParseException(e);
}
mailSender.send(message);
}
}
3.Bean 配置文件
配置 mailSender bean、电子邮件模板,并指定 Gmail SMTP 服务器的电子邮件详细信息。
文件:Spring-Mail.xml
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="587" />
<property name="username" value="username" />
<property name="password" value="password" />
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
</props>
</property>
</bean>
<bean id="mailMail" class="com.mkyong.common.MailMail">
<property name="mailSender" ref="mailSender" />
<property name="simpleMailMessage" ref="customeMailMessage" />
</bean>
<bean id="customeMailMessage"
class="org.springframework.mail.SimpleMailMessage">
<property name="from" value="from@no-spam.com" />
<property name="to" value="to@no-spam.com" />
<property name="subject" value="Testing Subject" />
<property name="text">
<value>
<![CDATA[
Dear %s,
Mail Content : %s
]]>
</value>
</property>
</bean>
</beans>
4.运行它
package com.mkyong.common;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Spring-Mail.xml");
MailMail mm = (MailMail) context.getBean("mailMail");
mm.sendMail("Yong Mook Kim", "This is text content");
}
}
输出
Dear Yong Mook Kim,
Mail Content : This is text content
Attachment : log.txt
下载源代码
Download it – Spring-Email-Attachment-Example.zipTags : email springfreestar.config.enabled_slots.push({ placementName: "mkyong_leaderboard_btf", slotId: "mkyong_leaderboard_btf" });
Spring SetFactoryBean 示例
' SetFactoryBean '类为开发人员提供了在 Spring 的 Bean 配置文件中创建具体集合(HashSet 和 TreeSet)的方法。
这里有一个 ListFactoryBean 示例,它将在运行时实例化一个 HashSet,并将其注入到一个 Bean 属性中
package com.mkyong.common;
import java.util.Set;
public class Customer
{
private Set sets;
//...
}
Spring 的 bean 配置文件。
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="CustomerBean" class="com.mkyong.common.Customer">
<property name="sets">
<bean class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="targetSetClass">
<value>java.util.HashSet</value>
</property>
<property name="sourceSet">
<list>
<value>1</value>
<value>2</value>
<value>3</value>
</list>
</property>
</bean>
</property>
</bean>
</beans>
或者,您也可以使用 util schema 和
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.5.xsd">
<bean id="CustomerBean" class="com.mkyong.common.Customer">
<property name="sets">
<util:set set-class="java.util.HashSet">
<value>1</value>
<value>2</value>
<value>3</value>
</util:set>
</property>
</bean>
</beans>
记得包括 util 模式,否则您将遇到以下错误
Caused by: org.xml.sax.SAXParseException:
The prefix "util" for element "util:set" is not bound.
运行它…
package com.mkyong.common;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
ApplicationContext context = new ClassPathXmlApplicationContext("SpringBeans.xml");
Customer cust = (Customer)context.getBean("CustomerBean");
System.out.println(cust);
}
}
输出
Customer [sets=[3, 2, 1]] Type=[class java.util.HashSet]
您已经实例化了 HashSet 和,并在运行时将其注入到 Customer 的 sets 属性中。
下载源代码
Download It – Spring-SetFactoryBean-Example.zip (5KB) ## 参考
spring (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190225101247/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
spring simple JDBC template batch update()示例
在本教程中,我们将向您展示如何在 SimpleJdbcTemplate 类中使用batchUpdate()
。
参见 SimpleJdbcTemplate 类中的batchUpdate()
示例。
//insert batch example
public void insertBatch(final List<Customer> customers){
String sql = "INSERT INTO CUSTOMER " +
"(CUST_ID, NAME, AGE) VALUES (?, ?, ?)";
List<Object[]> parameters = new ArrayList<Object[]>();
for (Customer cust : customers) {
parameters.add(new Object[] {cust.getCustId(),
cust.getName(), cust.getAge()}
);
}
getSimpleJdbcTemplate().batchUpdate(sql, parameters);
}
或者,您可以直接执行 SQL。
//insert batch example with SQL
public void insertBatchSQL(final String sql){
getJdbcTemplate().batchUpdate(new String[]{sql});
}
Spring 的 bean 配置文件
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="customerSimpleDAO"
class="com.mkyong.customer.dao.impl.SimpleJdbcCustomerDAO">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mkyongjava" />
<property name="username" value="root" />
<property name="password" value="password" />
</bean>
</beans>
运行它
package com.mkyong.common;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mkyong.customer.dao.CustomerDAO;
import com.mkyong.customer.model.Customer;
public class App
{
public static void main( String[] args )
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Spring-Customer.xml");
CustomerDAO customerSimpleDAO =
(CustomerDAO) context.getBean("customerSimpleDAO");
Customer customer1 = new Customer(1, "mkyong1",21);
Customer customer3 = new Customer(2, "mkyong2",22);
Customer customer2 = new Customer(3, "mkyong3",23);
List<Customer>customers = new ArrayList<Customer>();
customers.add(customer1);
customers.add(customer2);
customers.add(customer3);
customerSimpleDAO.insertBatch(customers);
String sql = "UPDATE CUSTOMER SET NAME ='BATCHUPDATE'";
customerSimpleDAO.insertBatchSQL(sql);
}
}
在本例中,您将插入三个客户的记录,并批量更新所有客户的姓名。
下载源代码
Download it – Spring-SimpleJdbcTemplate-batchUpdate-Example.zip (15 KB)jdbc spring (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190310101928/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Spring SimpleJdbcTemplate 查询示例
这里有几个例子来展示如何使用 SimpleJdbcTemplate query()
方法从数据库中查询或提取数据。在 JdbcTemplate query()
中,您需要手动将返回的结果转换为所需的对象类型,并传递一个对象数组作为参数。在 SimpleJdbcTemplate 中,它更加用户友好和简单。
jdbctemplate vesus simplejdbctemplate
Please compare this SimpleJdbcTemplate example with this JdbcTemplate example.
1.查询单行
这里有两种方法向您展示如何从数据库中查询或提取单个行,并将其转换为模型类。
1.1 自定义行映射器
一般来说,总是建议实现 RowMapper 接口来创建自定义的 RowMapper 以满足您的需求。
package com.mkyong.customer.model;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
public class CustomerRowMapper implements RowMapper
{
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
Customer customer = new Customer();
customer.setCustId(rs.getInt("CUST_ID"));
customer.setName(rs.getString("NAME"));
customer.setAge(rs.getInt("AGE"));
return customer;
}
}
public Customer findByCustomerId(int custId){
String sql = "SELECT * FROM CUSTOMER WHERE CUST_ID = ?";
Customer customer = getSimpleJdbcTemplate().queryForObject(
sql, new CustomerParameterizedRowMapper(), custId);
return customer;
}
1.2 BeanPropertyRowMapper
在 SimpleJdbcTemplate 中,需要使用“ParameterizedBeanPropertyRowMapper”而不是“BeanPropertyRowMapper”。
public Customer findByCustomerId2(int custId){
String sql = "SELECT * FROM CUSTOMER WHERE CUST_ID = ?";
Customer customer = getSimpleJdbcTemplate().queryForObject(sql,
ParameterizedBeanPropertyRowMapper.newInstance(Customer.class), custId);
return customer;
}
2.查询多行
从数据库中查询或提取多行,并将其转换为列表。
2.1 ParameterizedBeanPropertyRowMapper
public List<Customer> findAll(){
String sql = "SELECT * FROM CUSTOMER";
List<Customer> customers =
getSimpleJdbcTemplate().query(sql,
ParameterizedBeanPropertyRowMapper.newInstance(Customer.class));
return customers;
}
3.查询单个值
从数据库中查询或提取单个列值。
3.1 单列名称
它展示了如何以字符串形式查询单个列名。
public String findCustomerNameById(int custId){
String sql = "SELECT NAME FROM CUSTOMER WHERE CUST_ID = ?";
String name = getSimpleJdbcTemplate().queryForObject(
sql, String.class, custId);
return name;
}
3.2 总行数
它展示了如何从数据库中查询总行数。
public int findTotalCustomer(){
String sql = "SELECT COUNT(*) FROM CUSTOMER";
int total = getSimpleJdbcTemplate().queryForInt(sql);
return total;
}
运行它
package com.mkyong.common;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.mkyong.customer.dao.CustomerDAO;
import com.mkyong.customer.model.Customer;
public class SimpleJdbcTemplateApp
{
public static void main( String[] args )
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Spring-Customer.xml");
CustomerDAO customerSimpleDAO =
(CustomerDAO) context.getBean("customerSimpleDAO");
Customer customerA = customerSimpleDAO.findByCustomerId(1);
System.out.println("Customer A : " + customerA);
Customer customerB = customerSimpleDAO.findByCustomerId2(1);
System.out.println("Customer B : " + customerB);
List<Customer> customerAs = customerSimpleDAO.findAll();
for(Customer cust: customerAs){
System.out.println("Customer As : " + customerAs);
}
List<Customer> customerBs = customerSimpleDAO.findAll2();
for(Customer cust: customerBs){
System.out.println("Customer Bs : " + customerBs);
}
String customerName = customerSimpleDAO.findCustomerNameById(1);
System.out.println("Customer Name : " + customerName);
int total = customerSimpleDAO.findTotalCustomer();
System.out.println("Total : " + total);
}
}
结论
SimpleJdbcTemplate 不是 JdbcTemplate 的替代品,它只是 java5 友好的补充。
下载源代码
Download it – Spring-SimpleJdbcTemplate-Querying-Example.zip (15 KB)jdbc spring
spring Test——如何在 jsonPath 中测试 JSON 数组
在 Spring 中,我们可以使用像hasItem
和hasSize
这样的 Hamcrest APIs 来测试 JSON 数组。查看以下示例:
package com.mkyong;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
//article : jsonpath in array
@Test
public void save_emptyAuthor_emptyPrice_400() throws Exception {
String bookInJson = "{\"name\":\"ABC\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(3)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a author")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a price")));
verify(mockRepository, times(0)).save(any(Book.class));
}
}
用 Spring Boot 2 号进行了测试
参考
Tags : json json array spring test unit test
春季教程
原文:http://web.archive.org/web/20230101150211/https://mkyong.com/tutorials/spring-tutorials/
Rod Johnson 创建的 Spring framework 是一个非常强大的控制反转(IoC)框架,可以帮助分离项目组件的依赖关系。
在这一系列教程中,它提供了许多关于使用 Spring 框架的分步示例和解释。
New Spring 3.0 Tutorials (23/06/2011)
Added many Spring 3.0 tutorials on using Spring EL, JavaConfig, AspectJ and Spring Object/XML mapping (oxm). For what’s new in Spring 3.0, you can refer to this official Spring 3.0 references.
弹簧快速启动
快速了解 Spring 框架开发的基础。
- Spring hello world 示例
Maven+Spring 2 . 5 . 6 hello world 示例。 - Spring 3.0 hello world 示例(Spring 3.0)
Maven+Spring 3.0 hello world 示例,新 Spring 3.0 开发需要什么。 - Spring 松耦合示例
一个展示演示 Spring 如何使组件松耦合的例子。
Spring JavaConfig (Spring 3.0)
Spring 3.0 支持 JavaConfig,现在您可以使用注释在 Spring 中进行配置。
- Spring 3 JavaConfig 示例
演示了在 Spring 中使用@Configuration 和@Bean 来定义 Bean - Spring 3 JavaConfig @Import 示例
演示了如何使用@Import 来组织模块化的 beans。
弹簧依赖注入(DI)
Spring 如何做依赖注入(DI)来管理对象依赖?
- Spring 依赖注入(DI)
Spring 如何通过 Setter 注入和 Constructor 注入来应用依赖注入(DI)设计模式。 - Spring DI via setter 方法
依赖注入一个 bean via setter 方法。 - Spring DI via 构造函数
依赖注入 bean via 构造函数。 - Spring 中的构造函数注入类型不明确
构造函数注入参数类型不明确的问题总是发生在包含多个具有许多参数的构造函数方法的 bean 中。
基本豆
您需要在 Spring Ioc 容器中使用的所有类都被认为是“bean ”,并在 Spring bean 配置文件中或通过注释进行声明。
- Spring bean 引用示例
bean 如何通过在相同或不同的 bean 配置文件中指定 bean 引用来相互访问。 - 春天给豆子属性注入价值
给豆子属性注入价值的三种方式。 - 加载多个 Spring bean 配置文件
开发人员总是将不同的 bean 配置文件归类到不同的模块文件夹中,这里有一个提示告诉你如何加载多个 Spring bean 配置文件。 - Spring inner bean 示例
每当一个 bean 仅用于一个特定的属性时,总是建议将其声明为一个 inner bean。 - Spring bean scopes 示例
bean scopes 用于决定哪种类型的 Bean 实例应该从 Spring 容器返回给调用者。 - Spring 集合(列表、集合、映射和属性)示例
将值注入集合类型(列表、集合、映射和属性)的示例。 - ListFactoryBean 示例
创建一个具体的列表集合类(ArrayList 和 LinkedList),并注入到 Bean 属性中。 - SetFactoryBean 示例
创建一个具体的集合类(HashSet 和 TreeSet),并注入到 Bean 属性中。 - MapFactoryBean 示例
创建一个具体的地图集合类(HashMap 和 TreeMap),并注入到 Bean 属性中。 - Spring 将日期注入 bean 属性–CustomDateEditor
通常,Spring 接受日期变量,这里有一个使用 custom Date editor 来解决它的技巧。 - Spring PropertyPlaceholderConfigurer 示例
将部署细节外化到一个属性文件中,并通过一种特殊的格式—${ variable }从 bean 配置文件中访问。 - Spring bean 配置继承
继承对于 bean 共享公共值、属性或配置非常有用。 - Spring 依赖检查
Spring 提供了 4 种依赖检查模式来确保 bean 中已经设置了所需的属性。 - Spring 依赖检查带@Required 批注
依赖检查在批注模式下。 - 自定义@Required-style 批注
创建一个自定义@Required-style 批注,相当于@Required 批注。 - Bean InitializingBean 和 DisposableBean 示例
在 Bean 初始化和销毁时执行某些操作。(界面) - Bean 初始化方法和销毁方法示例
在 Bean 初始化和销毁时执行某些操作。(XML) - Bean @PostConstruct 和@PreDestroy example
在 Bean 初始化和销毁时执行某些操作。(注释)
Spring 表达式语言(Spring 3.0)
Spring 3.0 引入了功能丰富而强大的表达式语言,称为 Spring expression language,或 Spring EL。
- Spring EL hello world 示例
快速开始使用 Spring 表达式语言(EL)。 - 春埃尔比恩引用示例
引用比恩,比恩属性使用点号(。)符号。 - Spring EL 方法调用示例
直接调用 bean 方法。 - Spring EL 操作符示例
Spring EL 支持大多数标准的关系、逻辑和数学操作符。 - Spring EL 三元运算符(if-then-else)示例
条件检查,if else then。 - Spring EL 数组、列表、地图示例
与地图和列表一起工作。 - Spring EL 正则表达式示例
正则表达式评估条件。 - 用表达式测试弹簧 EL parser
向你展示如何轻松测试弹簧 EL。
Spring 自动组件扫描
Spring 能够自动扫描、检测和注册您的 bean。
- Spring 自动扫描组件
使 Spring 能够自动扫描、检测和注册您的 beans。 - 自动扫描中的弹簧过滤组件
示例在自动扫描模式下过滤某些组件。
弹簧自动布线 Bean
Spring 的“自动连接”模式可以在 XML 和注释中自动连接或连接 beans。
- 春季自动布线豆
春季 5 种自动布线模式总结。 - Spring 按类型自动连接
如果一个 bean 的数据类型与其他 bean 属性的数据类型兼容,则自动连接它。 - Spring 按名称自动连接
如果一个 bean 的名称与另一个 bean 属性的名称相同,则自动连接它。 - Spring 由构造函数自动连接
实际上,它是由构造函数参数中的类型自动连接。 - Spring auto wiring by auto detect
如果找到默认的构造函数,则选择“autowire by constructor”,否则使用“autowire by type”。 - Spring auto wired with @ auto wired annotation
举例说明如何在注释中定义“自动布线”模式。 - Spring auto wiring @ Qualifier Example
确定哪个 bean 有资格在字段上自动连线的示例。
Spring AOP(面向方面编程)
Spring AOP 模块化了方面中的横切关注点。简单来说,一个拦截器来拦截一些方法。
- Spring AOP 示例–建议
关于不同类型 Spring 建议的示例和说明。 - Spring AOP 示例–切入点、顾问
关于 Spring 不同类型的切入点和顾问的示例和解释。 - Spring AOP 拦截器序列
AOP 拦截器的序列会影响功能性。 - 这是一个自动代理创建器的例子,可以为你的 beans 自动创建代理对象,有助于避免创建许多重复的代理对象。
Spring AOP + AspectJ 框架
AspectJ 从 Spring 2.0 开始支持,更加灵活和强大。然而,这个例子是在 Spring 3.0 中演示的。
- Spring AOP + AspectJ 注释示例(Spring 3.0)
一个示例向您展示如何将 AspectJ 注释与 Spring 框架集成。 - Spring AOP + AspectJ in XML 配置示例(Spring 3.0)
Spring AOP with AspectJ in XML base 配置。
Spring 对象/XML 映射器(Spring 3.0)
在 Spring 3.0 中,对象到 XML 映射(OXM)从 Spring Web 服务转移到了核心的 Spring 框架。
- Spring Object/XML 映射示例
Spring oxm + castor,将对象转换为 XML,反之亦然。
弹簧 JDBC 支架
Spring 提供了许多助手类来简化整个 JDBC 数据库操作。
- Spring + JDBC 的例子
一个展示如何整合 Spring 和 JDBC 的例子。 - JdbcTemplate + JdbcDaoSupport 示例
示例使用 Spring 的 JdbcTemplate 和 JdbcDaoSupport 类来简化整个 JDBC 数据库操作过程。 - JdbcTemplate 查询示例
下面是几个示例,展示如何使用 JdbcTemplate query()方法从数据库中查询或提取数据。 - JdbcTemplate batchUpdate()示例
一个 batchUpdate()示例展示了如何执行批量插入操作。 - SimpleJdbcTemplate 查询示例
以更加用户友好和简单的方式从数据库中查询或提取数据。 - SimpleJdbcTemplate batch update()示例
另一个使用 SimpleJdbcTemplate 的批量更新示例,simple JdbcTemplate 是对 JDBC template 的 java5 友好的补充。 - SimpleJdbcTemplate 中的命名参数示例
一个示例展示了如何使用命名参数作为 SQL 参数值,而这仅在 simple JDBC template 中受支持。
弹簧休眠支持
Spring 附带了许多方便的类来支持 Hibernate ORM 框架。
- Maven+Spring+Hibernate+MySql 示例
一个使用 Spring 和 Hibernate 的简单项目。 - Maven+(Spring+Hibernate)Annotation+MySql 示例
一个使用 Spring 和 Hibernate 的简单项目(Annotation 版)。 - Spring AOP 在 Hibernate 中的事务管理
一个例子展示了如何用 Spring AOP 管理 Hibernate 事务。 - Struts + Spring + Hibernate 集成
用 Struts 和 Hibernate 框架集成 Spring 的例子。
Spring 电子邮件支持
Spring 提供了 MailSender 来通过 JavaMail API 发送电子邮件。
- 通过 MailSender 发送电子邮件
示例使用 Spring 的 MailSender 通过 Gmail SMTP 服务器发送电子邮件。 - bean 配置文件中的电子邮件模板
在方法体中硬编码所有的电子邮件属性和消息内容并不是一个好的做法,你应该考虑在 Spring 的 bean 配置文件中定义电子邮件消息模板。 - 发送带有附件的电子邮件
示例使用 Spring 发送带有附件的电子邮件。
春季调度支持
Spring 在 JDK 计时器和石英框架中都有很好的支持。
- Spring + JDK 定时器调度器示例
一篇关于 Spring 如何用 JDK 定时器调度作业的文章。 - Spring + Quartz 调度器示例
一篇关于 Spring 如何用 Quartz 框架调度作业的文章。 - Spring + Struts + Quartz 调度器示例
集成 Spring 和 Struts,用 Quartz 框架调度作业。
将 Spring 与其他 Web 框架集成
Spring 集成了其他 web 框架。
- servlet 会话监听器中的 Spring 依赖注入
Spring 附带了一个“ContextLoaderListener”监听器,作为在会话监听器和几乎所有其他 web 框架中启用 Spring 依赖注入的通用方式。 - Struts + Spring 集成
用 Struts 1.x 框架集成 Spring 的例子。 - Struts 2 + Spring 集成示例
将 Spring 与 Struts 2 框架集成的示例。 - JSF 2.0 + Spring 集成示例
将 JSF 2.0 与 Spring 框架集成的示例。 - JSF 2.0 + Spring + Hibernate 集成示例
将 JSF 2.0 + Spring + Hibernate 框架集成在一起的示例。 - Wicket + Spring 集成示例
Wicket 与 Spring 框架集成的示例。 - Struts 2 + Spring + Quartz 调度器集成示例
集成 Spring + Struts 2 + Quartz 的示例。 - Struts 2 + Spring + Hibernate 集成示例
集成 Spring + Struts 2 + Hibernate 的示例。
春季常见问题
- 在 Eclipse 中安装 Spring IDE
一篇关于如何在 Eclipse 中安装 Spring IDE 的文章。 - 带有 ResourceBundleMessageSource 的资源包示例
ResourceBundleMessageSource 是为不同地区解析文本消息的最常见的类。 - 在 bean 中访问 MessageSource(MessageSourceAware)
这个例子展示了如何通过 MessageSourceAware 接口在 bean 中获取 message source。 - 带有 getResource()的资源加载器示例
Spring 的资源加载器提供了一个非常通用的 getResource()方法来从文件系统、类路径或 URL 中获取资源,比如(文本文件、媒体文件、图像文件……)。
Spring 常见错误
一些 Spring 常见的错误消息。
- ClassNotFoundException:org . spring framework . web . context . context loader listener
- 无法代理目标类,因为 CGLIB2 不可用
- 需要 CGLIB 来处理@Configuration 类
- Java . lang . classnotfoundexception:org . exolab . castor . XML . XML exception
- Java . lang . classnotfoundexception:org . Apache . XML . serialize . XML serializer
弹簧参考
Spring @Value 默认值
原文:http://web.archive.org/web/20230101150211/https://mkyong.com/spring3/spring-value-default-value/
在本教程中,我们将向您展示如何为@Value
设置默认值
1.@价值示例
要在弹簧表达式中设置默认值,使用Elvis operator
:
#{expression?:default value}
几个例子:
@Value("#{systemProperties['mongodb.port'] ?: 27017}")
private String mongodbPort;
@Value("#{config['mongodb.url'] ?: '127.0.0.1'}")
private String mongodbUrl;
@Value("#{aBean.age ?: 21}")
private int age;
P.S @Value
自春季 3.0 开始发售
2.@值和属性示例
要设置属性占位符的默认值:
${property:default value}
几个例子:
//@PropertySource("classpath:/config.properties}")
//@Configuration
@Value("${mongodb.url:127.0.0.1}")
private String mongodbUrl;
@Value("#{'${mongodb.url:172.0.0.1}'}")
private String mongodbUrl;
@Value("#{config['mongodb.url']?:'127.0.0.1'}")
private String mongodbUrl;
config.properties
mongodb.url=1.2.3.4
mongodb.db=hello
对于“配置”bean。
<util:properties id="config" location="classpath:config.properties"/>
跟进
必须在 XML 或注释中注册一个静态PropertySourcesPlaceholderConfigurer
bean,以便 Spring @Value
知道如何解释${}
//@PropertySource("classpath:/config.properties}")
//@Configuration
@Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigIn() {
return new PropertySourcesPlaceholderConfigurer();
}
参考
Spring WebFlux 测试——阻塞读取超时 5000 毫秒
用WebTestClient
测试一个 Spring Webflux 端点,并显示以下错误消息。这有可能增加超时时间吗?
java.lang.IllegalStateException: Timeout on blocking read for 5000 MILLISECONDS
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:117)
at reactor.core.publisher.Mono.block(Mono.java:1524)
at org.springframework.test.web.reactive.server.ExchangeResult.formatBody(ExchangeResult.java:247)
at org.springframework.test.web.reactive.server.ExchangeResult.toString(ExchangeResult.java:216)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at org.springframework.test.web.reactive.server.ExchangeResult.assertWithDiagnostics(ExchangeResult.java:200)
解决办法
默认情况下,WebTestClient
会在 5 秒后超时。我们可以用@AutoConfigureWebTestClient
配置超时
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient(timeout = "10000")//10 seconds
public class TestCommentWebApplication {
@Autowired
private WebTestClient webClient;
参考
unit test webflux (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190308021305/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Java 8–将可选的转换为字符串
在 Java 8 中,我们可以使用.map(Object::toString)
将一个Optional<String>
转换成一个String
。
String result = list.stream()
.filter(x -> x.length() == 1)
.findFirst() // returns Optional
.map(Object::toString)
.orElse("");
样品
获得一个值的标准方法。
Java8Example1.java
package com.mkyong;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class Java8Example1 {
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
Optional<String> result = list.stream()
.filter(x -> x.length() == 1)
.findFirst();
if (result.isPresent()) {
System.out.println(result.get()); // a
}
}
}
输出
a
尝试map(Object::toString)
Java8Example2.java
package com.mkyong;
import java.util.Arrays;
import java.util.List;
public class Java8Example2 {
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
String s = list.stream().filter(x -> x.length() == 1)
.findFirst()
.map(Object::toString)
.orElse(null);
System.out.println(s); // a
}
}
输出
a
参考
- Java 8 流 findFirst()和 findAny()
- 可选的 JavaDoc
Struts 2 标签示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-a-tag-example/
Download It – Struts2-A-Tag-Example.zip
Struts 2 " 一个"标签用于呈现一个 HTML " <一个> "标签。最佳实践是始终使用“ < s:url > ”标签来创建 url 并将其嵌入到“ a 标签中。举个例子,
<s:url value="http://www.google.com" var="googleURL" />
<s:a href="%{googleURL}">Google</s:a>
在本教程中,它展示了使用 Struts 2 " a 标签的 3 种方法。
1.行动
转发请求的操作类。
ATagAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class ATagAction extends ActionSupport{
public String execute() {
return SUCCESS;
}
}
2.标签示例
一个 JSP 页面,展示了使用“ a 标签以不同的方式呈现 URL。
a.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 a tag example</h1>
<ol>
<li>
<s:url value="http://www.mkyong.com" var="mkyongURL" />
<s:a href="%{mkyongURL}">J2EE web development tutorials</s:a>
</li>
<li>
<s:a href="http://www.google.com">Google search engine</s:a>
</li>
<li>
<s:url action="aTagAction.action" var="aURL" />
<s:a href="%{aURL}">aTagAction</s:a>
</li>
</ol>
</body>
</html>
3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="aTagAction"
class="com.mkyong.common.action.aTagAction" >
<result name="success">pages/a.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/atag action . action
输出
在 HTML 源文件中输出
<html>
<head>
</head>
<body>
<h1>Struts 2 a tag example</h1>
<ol>
<li>
<a href="http://www.mkyong.com">J2EE web development tutorials</a>
</li>
<li>
<a href="http://www.google.com">Google search engine</a>
</li>
<li>
<a href="/Struts2Example/aTagAction.action">aTagAction</a>
</li>
</ol>
</body>
</html>
参考
Struts 2 操作标记示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-action-tag-example/
Download It – Struts2-Action-Tag-Example.zip
Struts 2 " action "标签用于直接从 JSP 页面调用 action 类。如果“ executeResult ”属性设置为 true,结果页面的内容将直接呈现在当前页面中。
使用一个完整的示例可以很好地说明这一点:
1.行动
一个 Action 类,用很少的方法将结果转发到不同的结果页面。
ParamTagAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class ActionTagAction extends ActionSupport{
public String execute() {
return SUCCESS;
}
public String sayHello(){
return "sayHello";
}
public String sayStruts2(){
return "sayStruts2";
}
public String saySysOut(){
System.out.println("SysOut SysOut SysOut");
return "saySysOut";
}
}
2.动作标签示例
JSP 页面展示了" action 标签的使用。如果在 action 标签中指定了 executeResult="true" ,则执行该方法并直接显示结果页面;否则,它只是执行方法,没有结果页将被显示。
action.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 action tag example</h1>
<ol>
<li>
Execute the action's result, render the page here.
<s:action name="sayHelloAction" executeResult="true"/>
</li>
<li>
Doing the same as above, but call action's sayStruts2() method.
<s:action name="sayHelloAction!sayStruts2" executeResult="true"/>
</li>
<li>
Call the action's saySysOut() method only, no result will be rendered,
By defautlt, executeResult="false".
<s:action name="sayHelloAction!saySysOut" />
</li>
</ol>
</body>
</html>
sayHello.jsp
<html>
<head>
</head>
<body>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2>Hello Hello Hello ~ from sayHello.jsp</h2>
</body>
</html>
sayStruts2.jsp
<html>
<head>
</head>
<body>
<h2>Struts 2 Struts 2 Struts 2 ~ from sayStruts2.jsp</h2>
</body>
</html>
saySysOut.jsp
<html>
<head>
</head>
<body>
<h2>SysOut SysOut SysOut ~ from saySysOut.jsp</h2>
</body>
</html>
3.struts.xml
声明了几个结果名来演示 executeResult 效果。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="actionTagAction"
class="com.mkyong.common.action.ActionTagAction" >
<result name="success">pages/action.jsp</result>
</action>
<action name="sayHelloAction"
class="com.mkyong.common.action.ActionTagAction"
method="sayHello">
<result name="sayHello">sayHello.jsp</result>
<result name="sayStruts2">sayStruts2.jsp</result>
<result name="saySysOut">saySysOut.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/actiontagaction . action
输出
参考
struts2 (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190304031539/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
struts 2 action error & action message 示例
Download It – Struts2-ActionError-ActionMessage-Example.zip
展示 Struts 2 的 ActionError 和 ActionMessage 类用法的教程。
1.action error–用于向用户发送错误反馈信息–通过 < s:actionerror/ > 显示。
<s:if test="hasActionErrors()">
<div class="errors">
<s:actionerror/>
</div>
</s:if>
2.action message——用于向用户发送信息反馈消息,通过 <显示 s:actionmessage/ > 。
<s:if test="hasActionMessages()">
<div class="welcome">
<s:actionmessage/>
</div>
</s:if>
这是一个简单的登录表单,如果用户名不等于“mkyong ”,则显示错误消息(actionerror ),否则重定向到另一个页面并显示欢迎消息(actionmessage)。此外,所有标签和错误消息都是从资源包(属性文件)中检索的。
1.文件夹结构
查看此项目结构
## 2.属性文件
存储消息的两个属性文件。
登录操作.属性
#Welcome messages
welcome.hello = Hello
#error message
username.required = Username is required
password.required = Password is required
global.properties
#Global messages
global.username = Username
global.password = Password
global.submit = Submit
global.reset = Reset
3.行动
一个经典的 action 类,做一个简单的检查确保用户名等于“mkyong”,用 addActionError() 设置错误消息或者用 addActionMessage() 设置成功消息。
package com.mkyong.user.action;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport{
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
//business logic
public String execute() {
return "SUCCESS";
}
//simple validation
public void validate(){
if("mkyong".equals(getUsername())){
addActionMessage("You are valid user!");
}else{
addActionError("I don't know you, dont try to hack me!");
}
}
}
4.JSP 视图
两个简单的 css 样式的 JSP 页面来定制错误消息。
login.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<style type="text/css">
.errors {
background-color:#FFCCCC;
border:1px solid #CC0000;
width:400px;
margin-bottom:8px;
}
.errors li{
list-style: none;
}
</style>
</head>
<body>
<h1>Struts 2 ActionError & ActionMessage Example</h1>
<s:if test="hasActionErrors()">
<div class="errors">
<s:actionerror/>
</div>
</s:if>
<s:form action="validateUser">
<s:textfield key="global.username" name="username"/>
<s:password key="global.password" name="password"/>
<s:submit key="global.submit" name="submit"/>
</s:form>
</body>
</html>
welcome.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<style type="text/css">
.welcome {
background-color:#DDFFDD;
border:1px solid #009900;
width:200px;
}
.welcome li{
list-style: none;
}
</style>
</head>
<body>
<h1>Struts 2 Struts 2 ActionError & ActionMessage Example</h1>
<s:if test="hasActionMessages()">
<div class="welcome">
<s:actionmessage/>
</div>
</s:if>
<h2>
<s:property value="getText('welcome.hello')" /> :
<s:property value="username"/>
</h2>
</body>
</html>
5.struts.xml
把所有的联系在一起。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.custom.i18n.resources" value="global" />
<package name="user" namespace="/user" extends="struts-default">
<action name="login">
<result>pages/login.jsp</result>
</action>
<action name="validateUser" class="com.mkyong.user.action.LoginAction">
<result name="SUCCESS">pages/welcome.jsp</result>
<result name="input">pages/login.jsp</result>
</action>
</package>
</struts>
In Struts 2, the functionality and usage of ActionError & ActionMessage are quite similar with Struts 1.
6.运行它
http://localhost:8080/struts 2 example/user/log in . action
用户名无效,显示带有的错误信息
用户名有效,显示欢迎消息
参考
Struts 2 和 JSON 示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-and-json-example/
在 Struts 2 示例中,您将学习如何通过“struts2-json-plugin.jar
”库将对象转换成 JSON 格式。
1.获取依赖库
获取 struts2-json-plugin.jar 库。
POM . XML
<!-- Struts 2 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.1.8</version>
</dependency>
<!-- Struts 2 JSON Plugins -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-json-plugin</artifactId>
<version>2.1.8</version>
</dependency>
2.行动(JSON)
这是一个将被转换成 JSON 格式的动作类。
package com.mkyong.common.action;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.opensymphony.xwork2.Action;
public class JSONDataAction{
private String string1 = "A";
private String[] stringarray1 = {"A1","B1"};
private int number1 = 123456789;
private int[] numberarray1 = {1,2,3,4,5,6,7,8,9};
private List<String> lists = new ArrayList<String>();
private Map<String, String> maps = new HashMap<String, String>();
//no getter method, will not include in the JSON
private String string2 = "B";
public JSONDataAction(){
lists.add("list1");
lists.add("list2");
lists.add("list3");
lists.add("list4");
lists.add("list5");
maps.put("key1", "value1");
maps.put("key2", "value2");
maps.put("key3", "value3");
maps.put("key4", "value4");
maps.put("key5", "value5");
}
public String execute() {
return Action.SUCCESS;
}
public String getString1() {
return string1;
}
public void setString1(String string1) {
this.string1 = string1;
}
public String[] getStringarray1() {
return stringarray1;
}
public void setStringarray1(String[] stringarray1) {
this.stringarray1 = stringarray1;
}
public int getNumber1() {
return number1;
}
public void setNumber1(int number1) {
this.number1 = number1;
}
public int[] getNumberarray1() {
return numberarray1;
}
public void setNumberarray1(int[] numberarray1) {
this.numberarray1 = numberarray1;
}
public List<String> getLists() {
return lists;
}
public void setLists(List<String> lists) {
this.lists = lists;
}
public Map<String, String> getMaps() {
return maps;
}
public void setMaps(Map<String, String> maps) {
this.maps = maps;
}
}
3.struts.xml
要输出 JSON 数据,需要声明一个扩展了“ json-default ”的包,结果类型为“ json ”。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="json-default">
<action name="getJSONResult"
class="com.mkyong.common.action.JSONDataAction">
<result type="json" />
</action>
</package>
</struts>
4.演示
访问动作 URL, JSONDataAction 的属性将被转换成 JSON 格式。
http://localhost:8080/struts 2 example/getjsonresult . action
JSON 格式…
{
"lists":["list1","list2","list3","list4","list5"],
"maps":
{
"key4":"value4","key3":"value3","key5":"value5","key2":"value2","key1":"value1"
},
"number1":123456789,
"numberarray1":[1,2,3,4,5,6,7,8,9],
"string1":"A",
"stringarray1":["A1","B1"]
}
Hope this super simple example can give you an overall idea of how JSON plugin worked with Struts 2. However, there are still many useful settings are not cover here, make sure you read the Struts 2 JSON plugin documentation for more details.
下载源代码
Download It – Struts2-JSON-Example.zip
参考
Struts 2 附加标签示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-append-tag-example/
Download It – Struts2-Append-Tag-Example.zip
Struts 2 append 标签用于将几个迭代器(由 List 或 Map 创建)组合成一个迭代器。在本教程中,您将使用 Struts 2 append 标签来完成以下任务:
- 将三个数组列表组合成一个迭代器。
- 将三个散列表组合成一个迭代器。
- 将 ArrayList 和 HashMap 组合成一个迭代器。
Assume 2 iterators, each has two entries, after combine with append tag into a single iterator, the order of the entries will look like following :
- 第一个迭代器的第一个条目。
- 第一个迭代器的第二个条目。
- 第二个迭代器的第一个条目。
- 第二个迭代器的第二个条目。
这只适用于列表迭代器;地图迭代器,顺序会随机。
1.行动
一个具有 3 个 ArrayList 和 3 个 HashMap 属性的 Action 类。
追加动作
package com.mkyong.common.action;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.opensymphony.xwork2.ActionSupport;
public class AppendTagAction extends ActionSupport{
private List<String> list1 = new ArrayList<String>();
private List<String> list2 = new ArrayList<String>();
private List<String> list3 = new ArrayList<String>();
private Map<String,String> map1 = new HashMap<String,String>();
private Map<String,String> map2 = new HashMap<String,String>();
private Map<String,String> map3 = new HashMap<String,String>();
public String execute() {
list1.add("List1 - 1");
list1.add("List1 - 2");
list1.add("List1 - 3");
list2.add("List2 - 1");
list2.add("List2 - 2");
list2.add("List2 - 3");
list3.add("List3 - 1");
list3.add("List3 - 2");
list3.add("List3 - 3");
map1.put("map1-key1", "map1-value1");
map1.put("map1-key2", "map1-value2");
map1.put("map1-key3", "map1-value3");
map2.put("map2-key1", "map2-value1");
map2.put("map2-key2", "map2-value2");
map2.put("map2-key3", "map2-value3");
map3.put("map3-key1", "map3-value1");
map3.put("map3-key2", "map3-value2");
map3.put("map3-key3", "map3-value3");
return SUCCESS;
}
//getter methods...
}
2.附加标签示例
一个 JSP 页面,展示了如何使用 append 标记将 3 ArrayList/3 HashMap/1 ArrayList+1 HashMap 组合成一个迭代器,并循环遍历其值并打印出来。
appendIterator.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 Append tag example</h1>
1\. Combine 3 ArrayList into a single iterator.
<s:append var="customListIterator">
<s:param value="%{list1}" />
<s:param value="%{list2}" />
<s:param value="%{list3}" />
</s:append>
<ol>
<s:iterator value="%{#customListIterator}">
<li><s:property /></li>
</s:iterator>
</ol>
2\. Combine 3 HashMap into a single iterator.
<s:append var="customMapIterator">
<s:param value="%{map1}" />
<s:param value="%{map2}" />
<s:param value="%{map3}" />
</s:append>
<ol>
<s:iterator value="%{#customMapIterator}">
<li><s:property /></li>
</s:iterator>
</ol>
3\. Combine ArrayList and HashMap into a single iterator.
<s:append var="customMixedIterator">
<s:param value="%{list1}" />
<s:param value="%{map1}" />
</s:append>
<ol>
<s:iterator value="%{#customMixedIterator}">
<li><s:property /></li>
</s:iterator>
</ol>
</body>
</html>
3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="appendTagAction"
class="com.mkyong.common.action.AppendTagAction" >
<result name="success">pages/appendIterator.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/appendtagaction . action
输出
Struts 2 Append tag example
1\. Combine 3 ArrayList into a single iterator.
1\. List1 - 1
2\. List1 - 2
3\. List1 - 3
4\. List2 - 1
5\. List2 - 2
6\. List2 - 3
7\. List3 - 1
8\. List3 - 2
9\. List3 - 3
2\. Combine 3 HashMap into a single iterator.
1\. map1-key3=map1-value3
2\. map1-key1=map1-value1
3\. map1-key2=map1-value2
4\. map2-key2=map2-value2
5\. map2-key3=map2-value3
6\. map2-key1=map2-value1
7\. map3-key3=map3-value3
8\. map3-key1=map3-value1
9\. map3-key2=map3-value2
3\. Combine ArrayList and HashMap into a single iterator.
1\. List1 - 1
2\. List1 - 2
3\. List1 - 3
4\. map1-key3=map1-value3
5\. map1-key1=map1-value1
6\. map1-key2=map1-value2
参考
Struts 2 自动完成器示例
Download It – Struts2-AutoCompleter-Example.zip
在 Struts 2 中, < sx:autocompleter > 标签是一个组合框,当用户在文本框中输入时,它会自动提示下拉建议列表。
This feature is implemented by dojo library, So, make sure you include “struts2-dojo-plugin.jar” as dependency library, put “struts-dojo-tags” tag on top of the page and output its header information via <sx:head />.
举个例子,
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
<body>
<sx:autocompleter label="What's your lucky number?"
name="yourLuckyNumber" autoComplete="false"
list="{'1','12','13','14'}" />
产生以下 HTML
<html>
<head>
<script language="JavaScript" type="text/javascript">
// Dojo configuration
djConfig = {
isDebug: false,
bindEncoding: "UTF-8"
,baseRelativePath: "/Struts2Example/struts/dojo/"
,baseScriptUri: "/Struts2Example/struts/dojo/"
,parseWidgets : false
};
</script>
<script language="JavaScript" type="text/javascript"
src="/Struts2Example/struts/dojo/struts_dojo.js"></script>
<script language="JavaScript" type="text/javascript"
src="/Struts2Example/struts/ajax/dojoRequire.js"></script>
<link rel="stylesheet" href="/Struts2Example/struts/xhtml/styles.css"
type="text/css"/>
<script language="JavaScript" src="/Struts2Example/struts/utils.js"
type="text/javascript"></script>
<script language="JavaScript" src="/Struts2Example/struts/xhtml/validation.js"
type="text/javascript"></script>
<script language="JavaScript" src="/Struts2Example/struts/css_xhtml/validation.js"
type="text/javascript"></script>
</head>
...
<tr>
<td class="tdLabel">
<label for="resultAction_yourLuckyNumber" class="label">
What's your lucky number?:</label></td>
<td>
<select dojoType="struts:ComboBox" id="resultAction_yourLuckyNumber"
autoComplete="false" name="yourLuckyNumber"
keyName="yourLuckyNumberKey" visibleDownArrow="true" >
<option value="1">1</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
</select>
</td>
</tr>
<script language="JavaScript" type="text/javascript">
djConfig.searchIds.push("resultAction_yourLuckyNumber");</script>
Struts 2 示例
一个完整的 < s:autocompleter > 标签的例子,当用户在相应的文本框中输入时,生成下拉建议列表。
1.pom.xml
下载 Struts 2 dojo 依赖库。
pom.xml
//...
<!-- Struts 2 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.1.8</version>
</dependency>
<!-- Struts 2 Dojo Ajax Tags -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-dojo-plugin</artifactId>
<version>2.1.8</version>
</dependency>
//...
2.动作类
Action 类来为" autocompleter "组件生成 web 框架选项列表。
AutoCompleterAction.java
package com.mkyong.common.action;
import java.util.ArrayList;
import java.util.List;
import com.opensymphony.xwork2.ActionSupport;
public class AutoCompleterAction extends ActionSupport{
private List<String> webframeworks = new ArrayList<String>();
private String yourFavWebFramework;
private String yourLuckyNumber;
public AutoCompleterAction(){
webframeworks.add("Spring MVC");
webframeworks.add("Struts 1.x");
webframeworks.add("Struts 2.x");
webframeworks.add("JavaServer Faces (JSF)");
webframeworks.add("Google Web Toolkit (GWT)");
webframeworks.add("Apache Wicket");
webframeworks.add("Apache Click");
webframeworks.add("Apache Cocoon");
webframeworks.add("JBoss Seam");
webframeworks.add("Stripes");
webframeworks.add("Apache Tapestry");
webframeworks.add("Others");
}
public String getYourLuckyNumber() {
return yourLuckyNumber;
}
public void setYourLuckyNumber(String yourLuckyNumber) {
this.yourLuckyNumber = yourLuckyNumber;
}
public String getYourFavWebFramework() {
return yourFavWebFramework;
}
public void setYourFavWebFramework(String yourFavWebFramework) {
this.yourFavWebFramework = yourFavWebFramework;
}
public List<String> getWebframeworks() {
return webframeworks;
}
public void setWebframeworks(List<String> webframeworks) {
this.webframeworks = webframeworks;
}
public String display() {
return NONE;
}
}
3.结果页面
通过“ < s:autocompleter > ”标签渲染“ autocompleter ”组件,通过 Java list 和 OGNL 生成自动下拉建议列表。
autocompleter.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
<body>
<h1>Struts 2 autocompleter example</h1>
<s:form action="resultAction" namespace="/" method="POST" >
<sx:autocompleter label="What's your lucky number?"
name="yourLuckyNumber" autoComplete="false"
list="{'1','12','13','14','21','22','23','24',
'31','32','33','34','41','42','43','44'}" />
<sx:autocompleter label="What's your favorite web framework?"
list="webframeworks" name="yourFavWebFramework" />
<s:submit value="submit" name="submit" />
</s:form>
</body>
</html>
result.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
<h1>Struts 2 autocompleter example</h1>
<h2>
Lucky Number : <s:property value="yourLuckyNumber"/>
</h2>
<h2>
Web Appication Frameworks : <s:property value="yourFavWebFramework"/>
</h2>
</body>
</html>
3.struts.xml
全部链接起来~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="autoCompleterAction"
class="com.mkyong.common.action.AutoCompleterAction"
method="display">
<result name="none">pages/autocompleter.jsp</result>
</action>
<action name="resultAction"
class="com.mkyong.common.action.AutoCompleterAction" >
<result name="success">pages/result.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/autocomplete action . action
Here’s another example to show the use of JSON data to provide a list of the select options to the autocompleter component – Struts 2 autocompleter + JSON example.
参考
Struts 2 自动完成器+ JSON 示例
Download It – Struts2-AutoCompleter-JSON-Example.zip
在最后一个 Struts 2 autocompleter 示例中,您了解了如何通过 Java list 和 ONGL 表达式为 autocompleter 组件生成一个选择选项列表。或者,也可以通过 JSON 数据生成选择选项。
Before you proceed, make sure you understand the basic usage of autocompleter component and JSON plugin. Read the below articles.
Struts 2 自动完成器+ JSON 示例
在本教程中,您将使用 Struts 2 JSON 插件将一个对象转换成 JSON 格式,并将其传递给自动完成器组件。
1.获取依赖库
获取所有的依赖库。
pom.xml
<!-- Struts 2 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.1.8</version>
</dependency>
<!-- Struts 2 Dojo Ajax Tags -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-dojo-plugin</artifactId>
<version>2.1.8</version>
</dependency>
<!-- Struts 2 JSON Plugins -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-json-plugin</artifactId>
<version>2.1.8</version>
</dependency>
2.行动
一个稍后要转换成 JSON 格式的类,为自动完成器组件提供一个选择选项列表。
DatabaseJSON.java
package com.mkyong.common.action;
import java.util.HashMap;
import java.util.Map;
import com.opensymphony.xwork2.Action;
public class DatabaseJSON{
private Map<String, String> databases = new HashMap<String, String>();
public DatabaseJSON(){
databases.put("MySQL", "MySQL");
databases.put("Oracle", "Oracle");
databases.put("PostgreSQL", "PostgreSQL");
databases.put("Microsoft SQL Server", "Microsoft SQL Server");
databases.put("DB2", "DB2");
databases.put("Others", "Others");
}
public String execute() {
return Action.SUCCESS;
}
public Map<String, String> getDatabases() {
return databases;
}
public void setDatabases(Map<String, String> databases) {
this.databases = databases;
}
}
一个普通的 Action 类,只做重定向工作并存储 autocompleter 值。
AutoCompleterAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class AutoCompleterAction extends ActionSupport{
private String yourDatabase;
public String display() {
return NONE;
}
public String getYourDatabase() {
return yourDatabase;
}
public void setYourDatabase(String yourDatabase) {
this.yourDatabase = yourDatabase;
}
}
3.结果
这里有点棘手,使用一个" s:url "标签指向一个" databaseJSON "动作,它将以 JSON 格式返回一个选项列表。并通过 href="%{databaseList}" 将其链接到自动完成器组件。
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
<body>
<h1>Struts 2 autocompleter + JSON example</h1>
<s:form action="resultAction" namespace="/" method="POST" >
<s:url id="databaseList" action="databaseJSON" />
<sx:autocompleter label="What's your favorite Database Server?"
href="%{databaseList}" name="yourFavDatabase" />
<s:submit value="submit" name="submit" />
</s:form>
</body>
</html>
4.struts.xml
按如下方式配置操作和 JSON 提供程序:
databases
It means, convert the DatabaseJSON’s databases property into JSON format, but the entire object.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="json" namespace="/" extends="json-default">
<action name="databaseJSON"
class="com.mkyong.common.action.DatabaseJSON">
<result type="json" >
<param name="root">databases</param>
</result>
</action>
</package>
<package name="default" namespace="/" extends="struts-default">
<action name="autoCompleterAction"
class="com.mkyong.common.action.AutoCompleterAction"
method="display">
<result name="none">pages/autocompleter-json.jsp</result>
</action>
<action name="resultAction"
class="com.mkyong.common.action.AutoCompleterAction" >
<result name="success">pages/result.jsp</result>
</action>
</package>
</struts>
4.演示
访问动作 URL,现在 JSON 数据提供了自动完成器选择选项。
http://localhost:8080/struts 2 example/autocomplete action . action
或者,您可以通过以下 URL
直接访问 JSON 数据:http://localhost:8080/struts 2 example/database JSON . action
{
"PostgreSQL":"PostgreSQL",
"MySQL":"MySQL",
"Others":"Others",
"Oracle":"Oracle",
"Microsoft SQL Server":"Microsoft SQL Server",
"DB2":"DB2"
}
参考
Struts 2 bean 标记示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-bean-tag-example/
Download It – Struts2-Bean-Tag-Example.zip
Struts 2 " bean "标签用于在 JSP 页面中实例化一个类。在本教程中,您将使用“ bean ”标记实例化一个名为“ HelloBean ”的类,通过“ param ”元素设置其属性并打印出值。
1.简单豆
一个简单的类,稍后使用 bean 标签来实例化它。
地狱篇. java
package com.mkyong.common.action;
public class HelloBean{
private String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
2.行动
转发请求的操作类。
BeanTagAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class BeanTagAction extends ActionSupport{
public String execute() {
return SUCCESS;
}
}
2.Bean 标记示例
一个 JSP 页面,展示了如何使用“ bean ”标记实例化“ HelloBean ”。
In “bean” tag, you can assign a name to the bean via a “var” attribute, later you can access the bean via #var_bean_name , or its property value via #var_bean_name.property.
bean.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 Bean tag example</h1>
<s:bean name="com.mkyong.common.action.HelloBean" var="hello">
<s:param name="msg">Hello Bean Tag</s:param>
</s:bean>
The HelloBean's msg property value : <s:property value="#hello.msg"/>
</body>
</html>
3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="beanTagAction"
class="com.mkyong.common.action.BeanTagAction" >
<result name="success">pages/bean.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/bean tag action . action
输出
参考
Struts 2 中文本地化问题
一个 Struts 2 i18n 显示中文字符的本地化问题…
案例 1:包含特殊字符的属性文件
一个属性文件以中文存储用户名、密码和提交信息。这个属性文件是以 UTF 8 格式创建的,但是内容不是用 native2ascii 编码的。
让我们尝试通过几个 UI 标签来显示中文字符。视图页面被声明为以带有 HTML meta 标签的 UTF-8 格式显示。
...
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
...
<s:form action="validateUser">
<s:textfield key="global.username" name="username"/>
<s:password key="global.password" name="password"/>
<s:submit key="global.submit" name="submit" />
<div>Testing 1 : <s:property value="getText('global.username')" /></div>
<div>Testing 2 : <s:text name="global.password" /></div></br/>
</s:form>
...
<s:url id="localezhCN" namespace="/" action="locale" >
<s:param name="request_locale" >zh_CN</s:param>
</s:url>
...
<s:a href="%{localezhCN}" >Chinese</s:a>
...
结果
令人惊讶的是,下面三个 UI 标签能够正确显示中文消息,
<s:textfield key="global.username" name="username"/>
<s:password key="global.password" name="password"/>
Testing 2 : <s:text name="global.password" />
但是“ s:submit ”和“ getText() ”都无法显示?
According to Java’s i18n documentation, to display a special character correctly with resource bundle, it must be precessed with the native2ascii tool.
在深入研究了 TextProvider.getText() 源代码后,它使用 resource bundle.getString()从资源包中检索消息,因此不正确的消息是合理的。但是为什么“ s:text ”、“ s:textfield ”和“ s:password ”能够正确显示中文信息,为什么“ s:submit ”会失败呢?我脑子里的问题太多了,让我们来看案例 2…
案例 2:带有特殊字符的属性文件(编码)
这一次,使用 native2ascii 工具处理属性文件,并正确编码中文字符。
结果
结果完全相反,现在“ s:submit ”和“ getText() ”能够正确显示,但是其他 UI 组件都失败了。这是预期工作,因为 Struts 2 推荐 getText() 显示 i18n 或本地化消息。问题是,为什么“ s:submit 不一样?
支柱 2..怎么了?
以下是我心中的一些疑问…
- 为什么 s:submit 的表现如此不同?
- i18n 应该很简单,为什么 Struts 2 有这种问题?还是我误解了 Struts 2 i18n 的工作原理?
- 为什么资源包中的消息有这么多显示方式?为什么不直接用一种方法分组呢?在 Struts 1 中,只使用“bean:message ”,为什么在 Struts 2 中看起来如此复杂?
- Struts 2 支持 XML 资源包吗?我只是不喜欢使用 native2ascii 工具将数据编码为 UTF 8 格式,这使得属性文件不可读。Apache Wicket 在这个问题上做得非常好,可能 Struts 2 应该借鉴一下。
- 那么,如何在 Struts 2 中正确显示汉字呢? Many articles and tutorials are told that the following methods are able to display the message from resource bundle:
<s:text name="global.username"/>
<s:property value="getText('global.username')"/>
但是,这仅适用于英语或一些“类似英语(欧洲)”的字符,例如法国、德国。但是对于中文或日文的“大方块”字符,两种方法将返回完全不同的输出。真的不知道如何将 Struts 2 本地化为中文和日文。
更新于…(2010 年 6 月 16 日)
从百度搜索引擎中挖掘解决方案,在其中一篇中文文章中找到了间接解决方案——Struts 2 国际化实例
解决办法
问题出在 HTML meta 标签上,
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
In Struts 1, the above meta tag is required to display the UTF-8 data correctly, but this is not true for Struts 2.
在 Struts 2 中,meta 标签不再起作用,我们应该把 < %@页面 content type = " text/html;charset = UTF-8 "%>标签在查看页面的第一行。举个例子,
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
...
结果
所有的中文信息都显示正确。
对我之前问题的回答
- 为什么 s:submit 的表现如此不同?答:对此不予置评
- i18n 应该很简单,为什么 Struts 2 有这种问题?还是我误解了 Struts 2 i18n 的工作原理?
答:一定要把 < %@页 content type = " text/html;charset = UTF-8 "%>"在查看页面的第一行。 - 为什么资源包中的消息有这么多显示方式?为什么不直接用一种方法分组呢?在 Struts 1 中,只使用“bean:message ”,为什么在 Struts 2 中看起来如此复杂?
A: s:text,key,getText(),name…,都能够正确渲染中文或 UTF 8 编码的数据,只要确保在视图页面中放入正确的“charset”即可。我仍然倾向于只使用一种方法来控制消息包(比如 Struts 1),太多的等价选项只会让开发人员感到困惑。 - Struts 2 支持 XML 资源包吗?我只是不喜欢使用 native2ascii 工具将数据编码为 UTF 8 格式,这使得属性文件不可读。Apache Wicket 在这个问题上做得非常好,可能 Struts 2 应该借鉴一下。
答:希望 Struts 2 能在下一个版本支持 XML 资源包。 - 那么,如何在 Struts 2 中正确显示汉字呢?
答:见上解。
Download it – Struts2-i18n-issue-Example
参考
- http://www . mkyong . com/Java/Java-convert-Chinese-character-to-unicode-with-native 2 ascii/
- http://forums.sun.com/thread.jspa?threadID=5185040
- http://www . mail-archive . com/user @ struts . Apache . org/msg 85490 . html
- http://www . code ranch . com/t/452139/Struts/application resources-properties-utf-characters # 2013 557
- http://struts.apache.org/2.1.8/docs/localization.html
- http://hxzon 00 . blog . 163 . com/blog/static/10489241620088121449163/
multiple languages struts2 (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190601051037/https://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
struts 2–为操作类配置静态参数
Download It – Struts-Action-Static-ParamExample.zip
在某些情况下,您可能需要为动作类分配一些预定义的或静态的参数值。
定义了动作的静态参数
在 Struts 2 中,可以在 struts.xml 文件中进行配置,通过 < param > 标签,例如
struts.xml
<struts>
<constant name="struts.custom.i18n.resources" value="global" />
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="locale" class="com.mkyong.common.action.LocaleAction">
<result name="SUCCESS">pages/welcome.jsp</result>
<param name="EnglishParam">English</param>
<param name="ChineseParam">Chinese</param>
<param name="FranceParam">France</param>
</action>
</package>
</struts>
它将三个预定义参数值分配给一个 LocaleAction 操作类。
从操作中获取静态参数
为了从 struts.xml 中获取静态参数值,Action 类必须实现参数化接口。它可以通过地图属性或 JavaBean 属性来访问。
The Action’s static parameters is control by the staticParams Interceptor, which is included in the default stack “struts-default.xml”. ## 1.地图属性
在动作类初始化期间,staticParams 拦截器将通过 setParams() 方法将预定义参数值的引用传递给动作类。
//...
import com.opensymphony.xwork2.config.entities.Parameterizable;
public class LocaleAction implements Parameterizable{
Map<String, String> params;
//...
public void setParams(Map<String, String> params) {
this.params = params;
}
}
2.JavaBean 属性
在 Action 类初始化期间,如果您正确地创建了 getter 和 setter 方法,staticParams 拦截器会将预定义的参数值设置为对应于“param”元素的每个 JavaBean 属性。
//...
import com.opensymphony.xwork2.config.entities.Parameterizable;
public class LocaleAction implements Parameterizable{
String englishParam;
String chineseParam;
String franceParam;
public String getEnglishParam() {
return englishParam;
}
public void setEnglishParam(String englishParam) {
this.englishParam = englishParam;
}
public String getChineseParam() {
return chineseParam;
}
public void setChineseParam(String chineseParam) {
this.chineseParam = chineseParam;
}
public String getFranceParam() {
return franceParam;
}
public void setFranceParam(String franceParam) {
this.franceParam = franceParam;
}
//...
}
Struts 2 创建自己的拦截器
Download it – Struts2-Create-Own-Interceptor-Example.zip
在本教程中,它展示了如何在 Struts 2 中创建自己的拦截器。
总结步骤:
- 创建一个类实现com . open symphony . xwork 2 . interceptor . interceptor。
- 实现 intercept(ActionInvocation 调用)方法。
- 在 struts.xml 中配置拦截器。
- 把它和行动联系起来。
Struts 2 interceptors
Struts 2 comes with many ready interceptors, make sure you check the list of the available Struts 2 interceptors before you create your own interceptor.
创建自己的拦截器的完整示例:
1.行动
转发用户请求和打印消息的简单操作。
HelloAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class HelloAction extends ActionSupport{
public String execute() throws Exception {
System.out.println("HelloAction execute() is called");
return SUCCESS;
}
}
2.拦截机
一个完整的拦截器示例。
PrintMsgInterceptor.java
package com.mkyong.common.interceptor;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.Interceptor;
public class PrintMsgInterceptor implements Interceptor{
//called during interceptor destruction
public void destroy() {
System.out.println("CustomInterceptor destroy() is called...");
}
//called during interceptor initialization
public void init() {
System.out.println("CustomInterceptor init() is called...");
}
//put interceptor code here
public String intercept(ActionInvocation invocation) throws Exception {
System.out.println("CustomInterceptor, before invocation.invoke()...");
String result = invocation.invoke();
System.out.println("CustomInterceptor, after invocation.invoke()...");
return result;
}
}
解释
拦截器类必须实现com . open symphony . xwork 2 . interceptor . interceptor 接口。拦截器初始化时,调用init();拦截器破坏, destroy() 被调用。最后,将所有完成工作的拦截器代码放在intercept(action invocation invocation)方法中。
invocation.invoke()
In the interceptor intercept() method, you must called the invocation.invoke() and return it’s result. This is the method responsible for calling the next interceptor or the action. The action will failed to continue without calling the invocation.invoke() method.destroy() is not reliable
It’s not recommend to put any code inside the destroy(), because this method is not reliable. When your application server is force shutdown or be killed by command, the destroy() will not be called. ## 3.struts.xml
在 struts.xml 文件中配置拦截器。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="default" namespace="/" extends="struts-default">
<interceptors>
<interceptor name="printMsgInterceptor"
class="com.mkyong.common.interceptor.PrintMsgInterceptor"></interceptor>
<interceptor-stack name="newStack">
<interceptor-ref name="printMsgInterceptor"/>
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<action name="helloAction"
class="com.mkyong.common.action.HelloAction" >
<interceptor-ref name="newStack"/>
<result name="success">pages/hello.jsp</result>
</action>
</package>
</struts>
4.演示
在服务器初始化期间,拦截器 init() 方法被调用。
INFO: Overriding property struts.i18n.reload - old value: false new value: true
15 Julai 2010 11:37:42 AM com.opensymphony.xwork2.util.logging.commons.CommonsLogger info
INFO: Overriding property struts.configuration.xml.reload - old value: false new value: true
CustomInterceptor init() is called...
15 Julai 2010 11:37:42 AM org.apache.coyote.http11.Http11Protocol start
INFO: Starting Coyote HTTP/1.1 on http-8080
15 Julai 2010 11:37:42 AM org.apache.jk.common.ChannelSocket init
INFO: JK: ajp13 listening on /0.0.0.0:8009
15 Julai 2010 11:37:42 AM org.apache.jk.server.JkMain start
INFO: Jk running ID=0 time=0/20 config=null
15 Julai 2010 11:37:42 AM org.apache.catalina.startup.Catalina start
INFO: Server startup in 994 ms
当您通过 URL 访问操作时:http://localhost:8080/struts 2 example/hello action . action
INFO: Overriding property struts.i18n.reload - old value: false new value: true
15 Julai 2010 11:37:42 AM com.opensymphony.xwork2.util.logging.commons.CommonsLogger info
INFO: Overriding property struts.configuration.xml.reload - old value: false new value: true
CustomInterceptor init() is called...
15 Julai 2010 11:37:42 AM org.apache.coyote.http11.Http11Protocol start
INFO: Starting Coyote HTTP/1.1 on http-8080
15 Julai 2010 11:37:42 AM org.apache.jk.common.ChannelSocket init
INFO: JK: ajp13 listening on /0.0.0.0:8009
15 Julai 2010 11:37:42 AM org.apache.jk.server.JkMain start
INFO: Jk running ID=0 time=0/20 config=null
15 Julai 2010 11:37:42 AM org.apache.catalina.startup.Catalina start
INFO: Server startup in 994 ms
CustomInterceptor, before invocation.invoke()...
HelloAction execute() is called
CustomInterceptor, after invocation.invoke()...
参考
Struts 2 日期标记示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-date-tag-example/
Download It – Struts2-Date-Tag-Example.zip
Struts 2 " date 标签用于以两种方式格式化日期对象:
- 自定义日期格式(如“日/月/年”)。
- “nice”属性将日期格式化为易于阅读的符号,如“此日期是 162 天前”。
在本教程中,它展示了使用 Struts 2 " date "标记将日期对象格式化为"自定义日期格式"和"易读符号"。
1.行动
转发请求的操作类,并用预定义的日期初始化日期对象。
date tagging . Java
package com.mkyong.common.action;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import com.opensymphony.xwork2.ActionSupport;
public class DateTagAction extends ActionSupport{
public Date customDate;
public String execute() {
Calendar cal = Calendar.getInstance();
//set date to january 31, 2010
cal.set(2010, 0, 31);
Date newDate = cal.getTime();
setCustomDate(newDate);
return SUCCESS;
}
public Date getCustomDate() {
return customDate;
}
public void setCustomDate(Date customDate) {
this.customDate = customDate;
}
}
2.日期标签示例
一个 JSP 页面,展示了如何使用“ date 标记来格式化日期对象:
- 默认日期格式。
- 自定义日期格式。
- 易读的符号。
date.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 date tag example</h1>
<ol>
<li>
Default date format
--> <strong><s:date name="customDate" /></strong>
</li>
<li>
Date format in "dd/MM/yyyy"
--> <strong><s:date name="customDate" format="dd/MM/yyyy" /></strong>
</li>
<li>
In Date tag, set the nice attribute to "true"
--> <strong><s:date name="customDate" nice="true" /></strong>
</li>
</ol>
</body>
</html>
3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="dateTagAction"
class="com.mkyong.common.action.DateTagAction" >
<result name="success">pages/date.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/datetagaction . action
输出
参考
Struts 2 日期时间选择器示例
Download It – Struts2-DateTimePicker-Example.zip
在 Struts 2 中,dojo ajax 标签“<sx:datetime picker>”会呈现一个文本框并在后面追加一个日历图标,点击日历图标会提示一个日期时间选择器组件。
创建一个日期时间选择组件,只需确保:
1。下载 struts2-dojo-plugin.jar 库。
2。包括“struts-dojo-tags”标记,并输出其标题。
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
举个例子,
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
<body>
<sx:datetimepicker name="date2" label="Format (dd-MMM-yyyy)"
displayFormat="dd-MMM-yyyy" value="%{'2010-01-01'}"/>
...
产生了下面的 HTML,几个 dojo 和 JavaScript 库来创建一个日期时间选择组件。
<html>
<head>
<script language="JavaScript" type="text/javascript">
// Dojo configuration
djConfig = {
isDebug: false,
bindEncoding: "UTF-8"
,baseRelativePath: "/Struts2Example/struts/dojo/"
,baseScriptUri: "/Struts2Example/struts/dojo/"
,parseWidgets : false
};
</script>
<script language="JavaScript" type="text/javascript"
src="/Struts2Example/struts/dojo/struts_dojo.js"></script>
<script language="JavaScript" type="text/javascript"
src="/Struts2Example/struts/ajax/dojoRequire.js"></script>
<link rel="stylesheet" href="/Struts2Example/struts/xhtml/styles.css" type="text/css"/>
<script language="JavaScript" src="/Struts2Example/struts/utils.js"
type="text/javascript"></script>
<script language="JavaScript" src="/Struts2Example/struts/xhtml/validation.js"
type="text/javascript"></script>
<script language="JavaScript" src="/Struts2Example/struts/css_xhtml/validation.js"
type="text/javascript"></script>
</head>
...
<td class="tdLabel">
<label for="widget_1291193434" class="label">Format (dd-MMM-yyyy):
</label></td>
<td>
<div dojoType="struts:StrutsDatePicker" id="widget_1291193434"
value="2010-01-01" name="date2" inputName="dojo.date2"
displayFormat="dd-MMM-yyyy" saveFormat="rfc"></div>
</td>
</tr>
<script language="JavaScript" type="text/javascript">
djConfig.searchIds.push("widget_1291193434");</script>
Struts 2 示例
一个完整的完整示例,使用 < s:datetimepicker > 标签生成一个 datetimepicker 组件,并展示如何使用 OGNL 和 Java 属性将默认日期设置为“ datetimepicker ”组件。
1.pom.xml
下载 Struts 2 dojo 依赖库。
pom.xml
//...
<!-- Struts 2 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.1.8</version>
</dependency>
<!-- Struts 2 Dojo Ajax Tags -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-dojo-plugin</artifactId>
<version>2.1.8</version>
</dependency>
//...
2.动作类
存储选定日期的操作类。
date time pick . Java
package com.mkyong.common.action;
import java.util.Date;
import com.opensymphony.xwork2.ActionSupport;
public class DateTimePickerAction extends ActionSupport{
private Date date1;
private Date date2;
private Date date3;
//return today date
public Date getTodayDate(){
return new Date();
}
//getter and setter methods
public String execute() throws Exception{
return SUCCESS;
}
public String display() {
return NONE;
}
}
3.结果页面
通过“ < s:datetimepicker > ”标签呈现日期时间选择器组件,通过 Java 属性和 OGNL 设置默认日期。
The ‘displayFormat‘ attribute is supported in many date patterns, read this article – Date Format Patterns.Ensure you put the “struts-dojo-tags” tag and render it’s header <sx:head />
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
datetimepicker.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>
<html>
<head>
<sx:head />
</head>
<body>
<h1>Struts 2 datetimepicker example</h1>
<s:form action="resultAction" namespace="/" method="POST" >
<sx:datetimepicker name="date1" label="Format (dd-MMM-yyyy)"
displayFormat="dd-MMM-yyyy" value="todayDate" />
<sx:datetimepicker name="date2" label="Format (dd-MMM-yyyy)"
displayFormat="dd-MMM-yyyy" value="%{'2010-01-01'}"/>
<sx:datetimepicker name="date3" label="Format (dd-MMM-yyyy)"
displayFormat="dd-MMM-yyyy" value="%{'today'}"/>
<s:submit value="submit" name="submit" />
</s:form>
</body>
</html>
result.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
<h1>Struts 2 datetimepicker example</h1>
<h2>
Date1 : <s:property value="date1"/>
</h2>
<h2>
Date 2 : <s:property value="date2"/>
</h2>
<h2>
Date 3 : <s:property value="date3"/>
</h2>
</body>
</html>
3.struts.xml
全部链接起来~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="dateTimePickerAction"
class="com.mkyong.common.action.DateTimePickerAction"
method="display">
<result name="none">pages/datetimepicker.jsp</result>
</action>
<action name="resultAction"
class="com.mkyong.common.action.DateTimePickerAction" >
<result name="success">pages/result.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/datetimepickeraction . action
参考
Struts 2 调试标记示例
原文:http://web.archive.org/web/20230101150211/http://www.mkyong.com/struts2/struts-2-debug-tag-example/
Download It – Struts2-Debug-Tag-Example.zip
在 Struts 2 中,“ debug 标签是一个非常有用的调试标签,用来输出“值栈的内容,以及 web 页面中的“栈上下文细节。在本教程中,它展示了在 JSP 页面中使用“ debug ”标记。
1.行动
一个简单的 Action 类,带有一个“ propertyInStack 属性,稍后显示在值堆栈中。
DebugTagAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class DebugTagAction extends ActionSupport{
public String propertyInStack;
public String getPropertyInStack() {
return propertyInStack;
}
public void setPropertyInStack(String propertyInStack) {
this.propertyInStack = propertyInStack;
}
}
2.日期标签示例
一个 JSP 页面,显示使用" debug "标签输出系统的"值栈"和"栈上下文"。
debug.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 debug tag example</h1>
<s:debug />
</body>
</html>
The <s:debug /> will generate a text link named “debug“, you need to click on the text link to expand the debugging details. ## 3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="debugTagAction"
class="com.mkyong.common.action.DebugTagAction" >
<result name="success">pages/debug.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/debugtagaction . action
输出
参考
struts 2–开发模式示例
在 Struts 2 开发中,这应该是要学习的第一个可配置值。为了启用 Struts 2 开发模式,您可以通过给予自动配置和属性文件重新加载和额外的日志记录和调试特性来显著提高您的 Struts 2 开发速度。
The auto reload feature is really a convenient feature. Each time i made changed in properties or XML configuration file, the application is no longer need to restart to take effect.By default, the Struts 2 development mode is disabled.
启用 Strut2 开发模式
在 struts 属性文件或 XML 配置文件中将" struts.devMode "值设置为 true。
struts.properties
struts.devMode = true
struts.xml
<struts>
<constant name="struts.devMode" value="true" />
</struts>
禁用 Strut2 开发模式
在 struts 属性文件或 XML 配置文件中将" struts.devMode "设置为 false。
struts.properties
struts.devMode = false
struts.xml
<struts>
<constant name="struts.devMode" value="false" />
</struts>
The development mode is only suitable in development or debugging environment. In production environment, you HAVE TO disabled it. It will caused significant impact on performance, because the entire application configuration, properties files will be reloaded on every request, many extra logging and debug information will be provide also.Before commit Struts configuration file, just make sure the development mode is turn off. I saw many accidentally commit cases – commit with development mode enable, and someone just grab the source code for QA environment. To be Frankly, QA seldom will do the performance test, they just make sure the functionality are there, and end with a development mode enabled application deployed to the production. Guess what? you will receive many screaming phone calls from clients very soon… ## 参考
Struts 2 下载文件示例
Download it – Struts2-Download-File-Example.zip
一个 Struts 2 的例子,展示了使用自定义结果类型来允许用户下载文件。
1.行动
在 Action 类中,声明了 InputStream 数据类型及其 getter 方法。
DownloadAction.java
package com.mkyong.common.action;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import com.opensymphony.xwork2.ActionSupport;
public class DownloadAction extends ActionSupport{
private InputStream fileInputStream;
public InputStream getFileInputStream() {
return fileInputStream;
}
public String execute() throws Exception {
fileInputStream = new FileInputStream(new File("C:\\downloadfile.txt"));
return SUCCESS;
}
}
2.查看页面
一个普通的页面,有一个下载文件的链接。
下载页. jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
<h1>Struts 2 download file example</h1>
<s:url id="fileDownload" namespace="/" action="download" ></s:url>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2>Download file - <s:a href="%{fileDownload}">fileABC.txt</s:a>
</h2>
</body>
</html>
3.struts.xml
定义下载文件细节,不言自明。值是动作的 InputStream 属性的名称。
Read this Struts 2 Stream Result documentation for more detail explanation.
struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="">
<result name="success">pages/downloadPage.jsp</result>
</action>
<action name="download" class="com.mkyong.common.action.DownloadAction">
<result name="success" type="stream">
<param name="contentType">application/octet-stream</param>
<param name="inputName">fileInputStream</param>
<param name="contentDisposition">attachment;filename="fileABC.txt"</param>
<param name="bufferSize">1024</param>
</result>
</action>
</package>
</struts>
4.运行它
http://localhost:8080/struts 2 example/
参考
- http://struts.apache.org/2.x/docs/stream-result.html
- http://www.iana.org/assignments/media-types/
- http://www . mkyong . com/struts/struts-download-file-from-website-example/
- http://www . mkyong . com/Java/how-to-download-file-from-website-Java-JSP/
- http://struts . Apache . org/2 . x/docs/how-can-we-return-a-text-string-as-the-response . html
download file struts2 (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190304031742/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Struts 2 execAndWait 拦截器示例
Download it – Struts2-ExecAndWait-Interceptor-Example.zip
Struts 2 附带了一个非常有趣的“执行和等待”拦截器,名为“执行和等待”,这是一个非常方便的拦截器,用于在后台长时间运行的操作,同时向用户显示一个自定义的等待页面。在本教程中,它展示了一个使用 Struts 2 execAndWait 拦截器的完整示例。
1.行动
一个普通的 action 类,用一个很长的运行过程来演示 execAndWait 效果。
LongProcessAction.java
package com.mkyong.common.action;
import com.opensymphony.xwork2.ActionSupport;
public class LongProcessAction extends ActionSupport{
public String execute() throws Exception {
//it should be delay few seconds,
//unless you have a super powerful computer.
for(int i =0; i<1000000; i++){
System.out.println(i);
}
return SUCCESS;
}
}
2.JSP 页面
创建两个页面:
- wait.jsp-在处理长时间运行的流程时向用户显示。
- success.jsp——流程完成后显示给用户。
HTML meta refresh
Remember to put the meta refresh on top of the waiting page; Otherwise, the page will not redirect to the success page, even the process is completed.
在这个wait.jsp中,元刷新被设置为每 5 秒重新加载一次页面,如果该过程完成,它将重定向到 success.jsp 的,否则停留在同一页面。
wait.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<meta http-equiv="refresh" content="5;url=<s:url includeParams="all" />"/>
</head>
<body>
<h1>Struts 2 execAndWait example</h1>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2>Please wait while we process your request...</h2>
</body>
</html>
success.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 execAndWait example</h1>
<h2>Done</h2>
</body>
</html>
3.执行和等待拦截器
链接 action 类并声明了" execAndWait "拦截器。
execAndWait parameters
- 延迟(可选):显示 wait.jsp 的初始延迟时间,以毫秒为单位。默认为无延迟。
- delaySleepInterval(可选):检查后台进程是否已经完成的间隔,以毫秒为单位。默认值为 100 毫秒。
struts.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="longProcessAction"
class="com.mkyong.common.action.LongProcessAction" >
<interceptor-ref name="execAndWait">
<param name="delay">1000</param>
<param name="delaySleepInterval">500</param>
</interceptor-ref>
<result name="wait">pages/wait.jsp</result>
<result name="success">pages/success.jsp</result>
</action>
</package>
</struts>
在这种情况下,它将延迟 1 秒显示wait.jsp,并每隔 500 毫秒检查一次后台进程是否已经完成。即使这个过程完成了,仍然需要等待 wait.jsp 的meta refresh 来激活页面重载。
4.演示
访问 URL:http://localhost:8080/struts 2 example/longprocessaction . action
延时 1 秒,显示wait.jsp。
过程完成后,自动显示success.jsp。
参考
interceptor struts2 (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190214232659/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Struts 2 文件上传示例
Download It – Struts2-File-Upload-Example.zip
在 Struts 2 中, < s:file > 标签用于创建一个 HTML 文件上传组件,允许用户从本地磁盘选择文件并上传到服务器。在本教程中,您将创建一个带有文件上传组件的 JSP 页面,设置上传文件的最大大小和允许的内容类型,并显示上传文件的详细信息。
1.动作类
Action 类用于文件上传,声明一个“file”变量来存储用户上传的文件,两个 String 变量来存储文件名和内容类型。“ fileUpload 拦截器”将通过 set“X”content type()和 set“X”FileName()自动注入上传的文件细节,确保方法名拼写正确。
P.S X 是存储上传文件的变量。
The file upload function is depends on the “fileUpload Interceptor“, make sure it is included in the Action’s stack. The lucky is, the default stack is already includes the “fileUpload Interceptor“.
FileUploadAction.java
package com.mkyong.common.action;
import java.io.File;
import com.opensymphony.xwork2.ActionSupport;
public class FileUploadAction extends ActionSupport{
private File fileUpload;
private String fileUploadContentType;
private String fileUploadFileName;
public String getFileUploadContentType() {
return fileUploadContentType;
}
public void setFileUploadContentType(String fileUploadContentType) {
this.fileUploadContentType = fileUploadContentType;
}
public String getFileUploadFileName() {
return fileUploadFileName;
}
public void setFileUploadFileName(String fileUploadFileName) {
this.fileUploadFileName = fileUploadFileName;
}
public File getFileUpload() {
return fileUpload;
}
public void setFileUpload(File fileUpload) {
this.fileUpload = fileUpload;
}
public String execute() throws Exception{
return SUCCESS;
}
public String display() {
return NONE;
}
}
2.结果页面
使用 < s:file > 标签渲染一个文件上传组件,将表单 enctype 类型设置为“multipart/form-data”。
fileupload.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<s:head />
</head>
<body>
<h1>Struts 2 <s:file> file upload example</h1>
<s:form action="resultAction" namespace="/"
method="POST" enctype="multipart/form-data">
<s:file name="fileUpload" label="Select a File to upload" size="40" />
<s:submit value="submit" name="submit" />
</s:form>
</body>
</html>
result.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
<h1>Struts 2 <s:file> file upload example</h1>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script><h2>
File Name : <s:property value="fileUploadFileName"/>
</h2>
<h2>
Content Type : <s:property value="fileUploadContentType"/>
</h2>
<h2>
File : <s:property value="fileUpload"/>
</h2>
</body>
</html>
3.struts.xml
全部链接起来~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<constant name="struts.custom.i18n.resources" value="global" />
<package name="default" namespace="/" extends="struts-default">
<action name="fileUploadAction"
class="com.mkyong.common.action.FileUploadAction" method="display">
<result name="none">pages/fileupload.jsp</result>
</action>
<action name="resultAction" class="com.mkyong.common.action.FileUploadAction">
<interceptor-ref name="exception"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="fileUpload">
<param name="allowedTypes">text/plain</param>
<param name="maximumSize">10240</param>
</interceptor-ref>
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*,^struts\..*</param>
</interceptor-ref>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<result name="success">pages/result.jsp</result>
<result name="input">pages/fileupload.jsp</result>
</action>
</package>
</struts>
文件大小限制
在本例中,您通过“ fileUpload interceptor 设置上传文件大小限制,该值以字节为单位计数。在这种情况下,上传文件的最大大小为 10kb。
The default maximum file size of the upload file is 2MB
文件类型
您也可以通过文件上传拦截器设置允许的文件类型。在这种情况下,上传文件只接受" text/plain 内容类型。
In Struts 2, there are may ways to lead same Rome, make sure you check this Struts 2 file upload documentation.
4.演示
http://localhost:8080/struts 2 example/file upload action . action
如果您上传的文件大于 10kb,或者不是文本文件,则会出现错误消息。
上传一个名为“XWORK-LICENSE.txt”的文本文件,文件大小:5kb。
上传的文件将被视为临时文件,具有长的随机文件名,upload _ _ 376584 a7 _ 12981122379 _ _ 8000 _ 0000010 . tmp。请确保将此临时文件复制到其他地方。阅读 FileUtils 文档轻松复制文件。
参考
- Struts 2 文件文档
- http://struts.apache.org/2.0.14/docs/file-upload.html
- http://struts . Apache . org/2 . 0 . 14/docs/how-do-we-upload-files . html
- http://commons . Apache . org/io/API-1.4/org/Apache/commons/io/fileutils . html
- http://www.mkyong.com/struts/struts-file-upload-example/
file upload struts2 (function (i,d,s,o,m,r,c,l,w,q,y,h,g) { var e=d.getElementById(r);if(e=null){ var t = d.createElement(o); t.src = g; t.id = r; t.setAttribute(m, s);t.async = 1;var n=d.getElementsByTagName(o)[0];n.parentNode.insertBefore(t, n); var dt=new Date().getTime(); try{i[l]w+y;}catch(er){i[h]=dt;} } else if(typeof i[c]!'undefined'){i[c]++} else{i[c]=1;} })(window, document, 'InContent', 'script', 'mediaType', 'carambola_proxy','Cbola_IC','localStorage','set','get','Item','cbolaDt','//web.archive.org/web/20190210101631/http://route.carambo.la/inimage/getlayer?pid=myky82&did=112239&wid=0')
Struts 2 生成器标记示例
Download It – Struts2-Generator-Tag-Example.zip
Struts 2 生成器标签用于根据页面中提供的“ val 属性生成一个迭代器。在本教程中,您将使用 Struts 2 生成器标签来完成以下任务:
- 用生成器标签创建一个迭代器。
- 创建一个带有生成器标签的迭代器,并用“转换器对象修改迭代器值。
1.行动
一个操作类,其方法返回一个“转换器对象。
生成或动作
package com.mkyong.common.action;
import org.apache.struts2.util.IteratorGenerator.Converter;
import com.opensymphony.xwork2.ActionSupport;
public class GeneratorTagAction extends ActionSupport{
public String execute() {
return SUCCESS;
}
public Converter getLanguageConverter(){
return new Converter() {
public Object convert(String value) throws Exception {
if("java".equals(value)){
return "[java value in converter] - " + value;
}else{
return value;
}
}
};
}
}
2.生成器标签示例
一个 JSP 页面,展示了如何使用生成器标签来动态创建迭代器。“分隔符属性是必需的,它将 val 分隔成迭代器的条目。
“转换器属性是可选的,允许您修改该值。在这种情况下,它将调用 GeneratorTagAction 的 getLanguageConverter() 方法,如果值等于“java”字符串,则修改该值。
generator.jsp
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
</head>
<body>
<h1>Struts 2 Generator tag example</h1>
1\. Generator tag example.
<s:generator val="%{'java|.net|c|python|shell'}" separator="|">
<ol>
<s:iterator>
<li><s:property /></li>
</s:iterator>
</s:generator>
</ol>
2\. Generator tag with converter example
<s:generator val="%{'java|.net|c|python|shell'}" separator="|"
converter="%{languageConverter}">
<ol>
<s:iterator>
<li><s:property /></li>
</s:iterator>
</s:generator>
</ol>
</body>
</html>
Can’t find any use case of this generator tag, as i don’t recommend to hardcore the iterator values in the page. ## 3.struts.xml
链接一下~
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="default" namespace="/" extends="struts-default">
<action name="appendTagAction"
class="com.mkyong.common.action.AppendTagAction" >
<result name="success">pages/appendIterator.jsp</result>
</action>
</package>
</struts>
4.演示
http://localhost:8080/struts 2 example/generatortagaction . action
参考
Struts 2 Hello World 注释示例
在本教程中,它将重用之前的 Strust 2 Hello World (XML 版本)示例,并将其转换为注释版本。
Download It – Struts2-Hello-World-Annotation-Example.zip
Struts 2 注释概念
Struts 2 注释是由 Struts 2 约定插件支持的,因此,您必须理解其“扫描方法学”和“命名转换器”机制背后的魔力。
1.扫描方法
许多 Struts 2 的文章或书籍指出,您可以配置过滤器的" init-param 或"Struts . conventi on . action . packages"来告诉 Struts 2 在哪里扫描带注释的类。举个例子,
web.xml
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
<init-param>
<param-name>actionPackages</param-name>
<param-value>com.mkyong.common</param-value>
</init-param>
</filter>
从我的测试(Struts2 版本 2.1.6 和 2.1.8)来看,这是不正确的,无论你在" param-value 或"Struts . conventi on . action . packages中放什么,struts2 都会忽略它,只扫描指定的名为 struts、Struts 2、action 或 actions 的文件夹。
这是扫描的工作原理
- 扫描位于名为“ struts,struts2,action 或 actions ”的包中的注释类。
- 接下来,扫描符合以下任一标准的文件:
- 实现了com . open symphony . xwork 2 . action接口。
- 扩展com . open symphony . xwork 2 . action support类。
- 文件名以动作结尾(如用户动作、登录动作)。
参见这个 Struts 2 约定插件文档。
2.命名转换器
Struts 2 约定插件将把所有带注释的动作文件名转换成指定的格式。
比如:LoginAction.java
- 首先,删除文件名末尾的“操作”一词(如果有)。
- 其次,将文件名的第一个字母转换成小写。
因此,在删除结尾并转换第一个字母的大小写后, LoginAction.action 将变为 login.action 。
The Struts 2 convention plugin’s “scanning methodology” and “naming converter” features are really bring a lot of conveniences and benefits, only if your Struts 2 project is following the naming convention properly; otherwise it will be a total disaster.
Struts 2 注释示例
是时候开始转换过程了。
最终项目结构
1.更新 pom.xml
要使用 Struts 2 注释特性,您需要下载Struts 2-conventi on-plugin . jar。
POM . XML
...
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.1.8</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-convention-plugin</artifactId>
<version>2.1.8</version>
</dependency>
...
2.登录操作
创建一个 LoginAction 扩展 ActionSupport 并且什么都不做, ActionSupport 默认返回一个“success”字符串,它将匹配@Result 并重定向到“ pages/login.jsp ”。
注释版本
package com.mkyong.user.action;
import org.apache.struts2.convention.annotation.Namespace;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.ResultPath;
import com.opensymphony.xwork2.ActionSupport;
@Namespace("/User")
@ResultPath(value="/")
@Result(name="success",location="pages/login.jsp")
public class LoginAction extends ActionSupport{
}
XML 等效物
<package name="user" namespace="/User" extends="struts-default">
<action name="Login">
<result>pages/login.jsp</result>
</action>
</package>
3.WelcomeUserAction
重写 execute()方法并指定@Action 和@Result 批注。
注释版本
package com.mkyong.user.action;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.Namespace;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.ResultPath;
import com.opensymphony.xwork2.ActionSupport;
@Namespace("/User")
@ResultPath(value="/")
public class WelcomeUserAction extends ActionSupport{
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Action(value="Welcome", results={
@Result(name="success",location="pages/welcome_user.jsp")
})
public String execute() {
return SUCCESS;
}
}
XML 等效物
<package name="user" namespace="/User" extends="struts-default">
<action name="Welcome" class="com.mkyong.user.action.WelcomeUserAction">
<result name="SUCCESS">pages/welcome_user.jsp</result>
</action>
</package>
The Struts 2 annotations – @Action, @Result and @Namespace are self-explanatory, you can always compare it with the XML equivalent. The @ResultPath may need a bit explanation, see this @ResultPath example.
4.JSP 视图页面
普通 JSP 视图页面接受用户名和密码,并在单击提交按钮后重定向到欢迎页面。
login.jsp
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
Struts 2 Hello World 注释示例