主模块(pom)
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>
	<groupId>com.imooc.security</groupId>
	<artifactId>imooc-security</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<packaging>pom</packaging>
	
	<properties>
		<imooc.security.version>1.0.0-SNAPSHOT</imooc.security.version>
	</properties>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>io.spring.platform</groupId>
				<artifactId>platform-bom</artifactId>
				<version>Brussels-SR4</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Dalston.SR2</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
	
	<modules>
		<module>../imooc-security-app</module>
		<module>../imooc-security-browser</module>
		<module>../imooc-security-core</module>
		<module>../imooc-security-demo</module>
	</modules>
</project>
核心业务模块core(jar)
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>imooc-security-core</artifactId>
	<parent>
		<groupId>com.imooc.security</groupId>
		<artifactId>imooc-security</artifactId>
		<version>1.0.0-SNAPSHOT</version>
		<relativePath>../imooc-security</relativePath>
	</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.social</groupId>
			<artifactId>spring-social-web</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-collections</groupId>
			<artifactId>commons-collections</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-beanutils</groupId>
			<artifactId>commons-beanutils</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
		</dependency>
	</dependencies>
</project>
登录返回类型枚举
/**
 * @author zhailiang
 *
 */
public enum LoginResponseType {
	
	/**
	 * 跳转
	 */
	REDIRECT,
	
	/**
	 * 返回json
	 */
	JSON
}
系统配置类
- 读取配置文件,如application.properties中的配置信息,封装为对象
//springboot配置类
@Configuration
//让读取配置文件的类生效
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
//security配置封装类
//指定配置文件中配置项的前缀,参见样例模块的application.properties
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
	//浏览器安全模块配置封装类
	private BrowserProperties browser = new BrowserProperties();
	
	private ValidateCodeProperties code = new ValidateCodeProperties();
	
	private SocialProperties social = new SocialProperties();
	public BrowserProperties getBrowser() {
		return browser;
	}
	public void setBrowser(BrowserProperties browser) {
		this.browser = browser;
	}
	public ValidateCodeProperties getCode() {
		return code;
	}
	public void setCode(ValidateCodeProperties code) {
		this.code = code;
	}
	public SocialProperties getSocial() {
		return social;
	}
	public void setSocial(SocialProperties social) {
		this.social = social;
	}
}
//浏览器安全模块配置封装类
public class BrowserProperties {
	
	private SessionProperties session = new SessionProperties();
	
	private String signUpUrl = "/imooc-signUp.html";
	//登录页
	private String loginPage = SecurityConstants.DEFAULT_LOGIN_PAGE_URL;
	//登录返回类型(跳转,json)
	private LoginResponseType loginType = LoginResponseType.JSON;
	
	private int rememberMeSeconds = 3600;
	public String getLoginPage() {
		return loginPage;
	}
	public void setLoginPage(String loginPage) {
		this.loginPage = loginPage;
	}
	public LoginResponseType getLoginType() {
		return loginType;
	}
	public void setLoginType(LoginResponseType loginType) {
		this.loginType = loginType;
	}
	public int getRememberMeSeconds() {
		return rememberMeSeconds;
	}
	public void setRememberMeSeconds(int rememberMeSeconds) {
		this.rememberMeSeconds = rememberMeSeconds;
	}
	public String getSignUpUrl() {
		return signUpUrl;
	}
	public void setSignUpUrl(String signUpUrl) {
		this.signUpUrl = signUpUrl;
	}
	public SessionProperties getSession() {
		return session;
	}
	public void setSession(SessionProperties session) {
		this.session = session;
	}
	
}
图片验证码
- 
    封装图形验证码信息: core.validate.code.image.ImageCode(封装图形验证码信息) core.validate.code.ValidateCode(封装随机数和过期时间) core.validate.code.ValidateCodeController(验证码获取controller) core.validate.code.ValidateCodeProcessorHolder(验证码处理器查询) core.validate.code.ValidateCodeFilter(验证码验证过滤器) browser.BrowserSecurityConfig(浏览器安全配置类) core.validate.code.ValidateCodeException(验证码异常) core.properties.ValidateCodeProperties(验证码配置类) core.properties.ImageCodeProperties(图片验证码默认配置类) core.properties.SecurityProperties 应用级配置application.properties: #imooc.security.code.image.length = 6#imooc.security.code.image.width = 100#imooc.security.code.image.url = /user/*core.validate.code.ValidateCodeGenerator(校验码生成器接口) core.validate.code.image.ImageCodeGenerator(图片验证码自动生成器) core.validate.code.ValidateCodeBeanConfig(验证码容器配置类) demo.code.DemoImageCodeGenerator(测试自己自定义的图形验证码生成逻辑) <tr> <td>图形验证码:</td> <td> <input type="text" name="imageCode"> <img src="/code/image?width=200"> </td> </tr>
记住我功能
基本原理
用户名密码认证过滤器:UsernamePasswordAuthenticationFilter
记住我认证过滤器:RememberMeAuthenticationFilter
浏览器--(认证请求)--用户名密码认证过滤器--(认证成功)--RemeberMeService(TokenRepository)--(将Token写入数据库,将Token写入浏览器Cookie)--数据库
浏览器--(服务请求)--记住我认证过滤器--(读取Cookie中的Token)--RemeberMeService(TokenRepository)--(查找Token)--数据库--UserDetailsService
- browser.BrowserSecurityConfig(配置和数据库交互的PersistentTokenRepository)
- core.properties.BrowserProperties(设置有效时长)
<tr>
<td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
</tr>
短信验证码登录
- 发送手机验证码:
- core.validate.code.sms.SmsCodeSender(短信验证码发送接口)
- core.validate.code.sms.DefaultSmsCodeSender(默认手机验证码发送实现类)
- core.validate.code.ValidateCodeBeanConfig(验证码容器配置类)
- core.validate.code.sms.SmsCodeGenerator(短信验证码生成器)
- core.properties.SmsCodeProperties(手机验证码默认配置类)
- core.properties.ValidateCodeProperties(验证码配置类)
- 验证手机验证码
- core.authentication.mobile.SmsCodeAuthenticationToken(封装手机验证码登录信息)
- core.authentication.mobile.SmsCodeAuthenticationFilter(手机验证登录过滤器)
- core.authentication.mobile.SmsCodeAuthenticationProvider(实现手机登录校验逻辑)
- core.authentication.mobile.SmsCodeAuthenticationSecurityConfig(手机验证码安全认证配置)
- browser.BrowserSecurityConfig
社交账号登录


QQ登录
- 第6步:获取用户信息Api
- core.social.qq.api.QQ(获取QQ用户消息接口)
- core.social.qq.api.QQUserInfo(QQ用户信息)
- core.social.qq.api.QQImpl(获取QQ用户信息实现类)
- 前五步获取连接
- core.social.qq.connet.QQServiceProvider(QQ服务提供类)
- core.social.qq.connet.QQAdapter(QQ适配器)
- core.social.qq.connet.QQConnectionFactory(QQ连接工厂)
- core.social.SocialConfig(社交配置)
- demo.security.MyUserDetailsService(用户详情信息Service)
- core.properties.QQProperties
- core.properties.SocialProperties
- core.properties.SecurityProperties
- core.social.qq.config.QQAutoConfig(QQ认证配置)
- application.properties中:
- imooc.security.social.weixin.app-id = wxd99431bbff8305a0 imooc.security.social.weixin.app-secret = 60f78681d063590a469f1b297feff3c4
- browser.BrowserSecurityConfig(浏览器安全配置类)
- core.social.ImoocSpringSocialConfigurer(重写点击QQ登录的URL)
- core.social.qq.connet.QQOAuth2Template
- 暂停第五章(1)
浏览器安全模块(jar)
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>imooc-security-browser</artifactId>
	<parent>
		<groupId>com.imooc.security</groupId>
		<artifactId>imooc-security</artifactId>
		<version>1.0.0-SNAPSHOT</version>
		<relativePath>../imooc-security</relativePath>
	</parent>
	<dependencies>
		<dependency>
			<groupId>com.imooc.security</groupId>
			<artifactId>imooc-security-core</artifactId>
			<version>${imooc.security.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.2.2</version>
		</dependency>
	</dependencies>
</project>
security基本原理
请求----SecurityContextPersistenceFilter----UsernamePasswordAuthenticationFilter(可选)----BasicAuthenticationFilter(可选)------其他过滤器-----ExceptionTranslationFilter(异常处理过滤器,必选)----FilterSecurityInterceptor-----REST API(Controller)
用户认证逻辑
- 
    处理用户信息获取逻辑(UserDetailsService) 处理用户校验逻辑(UserDetails) 处理密码加密解密(PasswordEncoder) 
//自实现用户详情
@Component
public class MyUserDetailsService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    //处理用户信息获取
    @Override
    public UserDetail loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("登录用户名"+username);
        //根据表单提交的用户名去数据库中查找用户信息
        //根据用户信息判断用户是否被冻结等
        //passwordEncoder.encode完成注册功能,返回的字符串是经过加密的
        String password = passwordEncoder.encode("123456");
        logger.info("用户的加密密码是:"+password);
        return new User(username,password,
                       true,true,true,true,
                       AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        
    }
}
//SecurityConfig配置类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    //密码加解密和比对
    @Bean
    public PasswordEncoder passwordEncoder(){
        //这是spring自己提供的密码工具类,可以自己写一个密码处理工具类
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        //使用表单登录,拦截所有request请求需要认证
        http.formLogin()
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated();
    }
}
用户认证流程
- 
    自定义登录页面(http.formLogin().loginPage) 自定义登录成功或失败处理(AuthenticationSuccessHandler,AuthenticationFailureHandler) 
- 
    处理不同类型的请求 
接到HTML请求或数据请求----跳转到一个自定义的Controller方法上----判断是否是HTML请求引发的跳转----(是)返回登录页面----(否)返回401状态码和错误信息
//SecurityConfig配置类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
	private SecurityProperties securityProperties;
    
    //密码加解密和比对
    @Bean
    public PasswordEncoder passwordEncoder(){
        //这是spring自己提供的密码工具类,可以自己写一个密码处理工具类
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        //使用表单登录,拦截所有request请求需要认证
        http.formLogin()
            //.loginPage("signIn.html")//指定登录页面
            //换为controller的URL
            .loginPage("/authentication/require")
            //对过滤器声明登录页面的表单action路径
            .loginProcessingUrl("/authentication/form")
            .and()
            .authorizeRequests()
            //登录页面请求不需要认证,放行
            .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage()).permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();//security自动开启身份防伪造安全功能,这里先关闭
    }
}
//自定义身份认证控制器
@RestController
public class BrowserSecurityController {
	private Logger logger = LoggerFactory.getLogger(getClass());
	//拿到引发跳转的请求
	private RequestCache requestCache = new HttpSessionRequestCache();
	//页面跳转工具
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
	//注入core模块中的配置文件封装类
	@Autowired
	private SecurityProperties securityProperties;
	@Autowired
	private ProviderSignInUtils providerSignInUtils;
	/**
	 * 当需要身份认证时,跳转到这里
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws IOException
	 */
	@RequestMapping("/authentication/require")
	@ResponseStatus(code = HttpStatus.UNAUTHORIZED)//返回401状态码
	public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws IOException {
		//获取到引发跳转的请求
        //比如启动项目访问/user,但是这时还没有登录或认证,security就会去执行身份认证逻辑
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest != null) {
            //获取到引发跳转的URL,比如:localhost:8080/user
			String targetUrl = savedRequest.getRedirectUrl();
			logger.info("引发跳转的请求是:" + targetUrl);
            //如果请求是一个页面请求,就跳转到指定页面
			if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                //因为security是一个公共的模块,所以不可能每次都跳转到一个写死的页面
                //这里的跳转页面应该是调用该模块的用户自己的登录页面
                //参见core模块
				redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
			}
		}
		//返回一个包裹错误信息的对象
		return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
	}
}
//一个包裹错误信息的对象
public class SimpleResponse {
	
	public SimpleResponse(Object content){
		this.content = content;
	}
	
	private Object content;
	public Object getContent() {
		return content;
	}
	public void setContent(Object content) {
		this.content = content;
	}
	
}
登录成功处理
- 
    根据用户配置来决定是跳转页面还是返回json 
- 
    参见core模块:LoginResponseType,BrowserProperties 样例模块:application.properties 
/**
 * 登录成功处理器
 *
 */
@Component("imoocAuthenticationSuccessHandler")
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());
	//spring自带的
	@Autowired
	private ObjectMapper objectMapper;
	@Autowired
	private SecurityProperties securityProperties;
	/*登录成功后会被调用
	 * (non-Javadoc)
	 */
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		logger.info("登录成功");
        //根据用户自己的application.properties配置的登录方式决定跳转页面或发送json
		if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            //响应形式为json
			response.setContentType("application/json;charset=UTF-8");
            //把authentication信息以json形式返回
			response.getWriter().write(objectMapper.writeValueAsString(authentication));
		} else {
            //跳转页面
			super.onAuthenticationSuccess(request, response, authentication);
		}
	}
}
- 让spring使用我们自己写的登录成功处理器,修改BrowserSecurityConfig
//SecurityConfig配置类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
	private SecurityProperties securityProperties;
    //把自己的登录成功处理器注入进来
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler imoocAuthenctiationFailureHandler;
    
    //密码加解密和比对
    @Bean
    public PasswordEncoder passwordEncoder(){
        //这是spring自己提供的密码工具类,可以自己写一个密码处理工具类
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        //使用表单登录,拦截所有request请求需要认证
        http.formLogin()
            //.loginPage("signIn.html")//指定登录页面
            //换为controller的URL
            .loginPage("/authentication/require")
            //对过滤器声明登录页面的表单action路径
            .loginProcessingUrl("/authentication/form")
            //指定自定义的登录成功和登录失败处理器
            .successHandler(imoocAuthenticationSuccessHandler)
            .failureHandler(imoocAuthenctiationFailureHandler)
            .and()
            .authorizeRequests()
            //登录页面请求不需要认证,放行
            .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage()).permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();//security自动开启身份防伪造安全功能,这里先关闭
    }
}
登录失败处理
//登录失败处理器
@Component("imoocAuthenctiationFailureHandler")
public class ImoocAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	@Autowired
	private ObjectMapper objectMapper;
	
	@Autowired
	private SecurityProperties securityProperties;
	
	/* (non-Javadoc)
	 */
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		logger.info("登录失败");
        //根据用户自己的application.properties配置的登录方式决定跳转页面或发送json
        if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
			//设置500错误码
			response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
			response.setContentType("application/json;charset=UTF-8");
        	//返回异常信息
			response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));
        }else{
			super.onAuthenticationFailure(request, response, exception);
		}
	}
}
- 修改BrowserSecurityConfig使处理器生效
app相关模块(jar)
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>imooc-security-app</artifactId>
	<parent>
		<groupId>com.imooc.security</groupId>
		<artifactId>imooc-security</artifactId>
		<version>1.0.0-SNAPSHOT</version>
		<relativePath>../imooc-security</relativePath>
	</parent>
	
	<dependencies>
		<dependency>
			<groupId>com.imooc.security</groupId>
			<artifactId>imooc-security-core</artifactId>
			<version>${imooc.security.version}</version>
		</dependency>
	</dependencies>
</project>
样例模块
pom
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>imooc-security-demo</artifactId>
	<parent>
		<groupId>com.imooc.security</groupId>
		<artifactId>imooc-security</artifactId>
		<version>1.0.0-SNAPSHOT</version>
		<relativePath>../imooc-security</relativePath>
	</parent>
	<dependencies>
		<dependency>
			<groupId>com.imooc.security</groupId>
			<artifactId>imooc-security-browser</artifactId>
			<version>${imooc.security.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.7.0</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.7.0</version>
		</dependency>
		<dependency>
			<groupId>com.github.tomakehurst</groupId>
			<artifactId>wiremock</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<version>1.3.3.RELEASE</version>
				<executions>
					<execution>
						<goals>
							<goal>repackage</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
		<finalName>demo</finalName>
	</build>
</project>
application.properties
#springboot配置
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/imooc-demo?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
spring.session.store-type = none
server.session.timeout = 600
#启用security.basic.enabled = false
server.port = 8060
#指定security身份认证时跳转到了登录页面
imooc.security.browser.signUpUrl = /demo-signUp.html
#指定登录时返回类型是跳转页面还是json
imooc.security.browser.loginType = REDIRECT
RestFul

常用注解
- 
    @RestController:标明此Controller提供RestAPI @RequestMapping:映射HTTP请求URL到java方法 @RequestParam:映射请求参数到java方法的参数 @PageableDefault:指定分页参数默认值 @PathVariable:映射URL片段到java方法的参数 @JsonView:控制json输出内容 @RequestBody:映射请求体到java方法的参数 @Valid+BindingResult:验证请求参数的合法性并处理校验结果 
测试用例
GET请求
- 设计测试用例测试查询
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
	
    //web应用上下文
	@Autowired
	private WebApplicationContext wac;
	//模拟MVC环境
	private MockMvc mockMvc;
	@Before
	public void setup() {
        //在执行测试方法之前实例化MVC环境
		mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}
	
	@Test
	public void whenQuerySuccess() throws Exception {
        //模拟发送RestFul格式请求
        //get("/user"):请求URL
        //param("username", "jojo"):请求参数
        //contentType:类型
        //andExpect:期望得到的响应状态
        //jsonPath:响应参数的长度为3
		String result = mockMvc.perform(
				get("/user").param("username", "jojo").param("age", "18").param("ageTo", "60").param("xxx", "yyy")
						// .param("size", "15")
						// .param("page", "3")
						// .param("sort", "age,desc")
						.contentType(MediaType.APPLICATION_JSON_UTF8))
				.andExpect(status().isOk()).andExpect(jsonPath("$.length()").value(3))
				.andReturn().getResponse().getContentAsString();
		
		System.out.println(result);
	}
}
- 根据id查询详细信息
@Test
	public void whenGetInfoSuccess() throws Exception {
		String result = mockMvc.perform(get("/user/1")
				.contentType(MediaType.APPLICATION_JSON_UTF8))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.username").value("tom"))
				.andReturn().getResponse().getContentAsString();
		
		System.out.println(result);
	}
//{id}:id可以是字符串,也可以是数字
//@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
//正则表达式,接收的参数必须为数字:
//@PathVariable:把URL中{id}映射到参数的id上
@GetMapping("/user/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@PathVariable String id) {
    User user = new User();
    user.setUsername("tom");
    return user;
}
POST请求
- 添加用户
	@Test
	public void whenCreateSuccess() throws Exception {
		
		Date date = new Date();
		System.out.println(date.getTime());
        //模拟前台发送的json数据,时间类型用时间戳传递,由前台决定保留哪些部分
		String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
		String reuslt = mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON_UTF8)
				.content(content))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.id").value("1"))
				.andReturn().getResponse().getContentAsString();
		
		System.out.println(reuslt);
	}
//@RequestBody:把前台传递的json串映射为一个对象
//@Valid:使用user对象中的校验参数注解
//BindingResult:当参数校验有异常时可以获取到异常信息
@PostMapping
@ApiOperation(value = "创建用户")
public User create(@Valid @RequestBody User user, BindingResult errors) {
    //如果有异常,打印所有异常
    if(errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user.getId());
    System.out.println(user.getUsername());
    System.out.println(user.getPassword());
    System.out.println(user.getBirthday());
    user.setId("1");
    return user;
}
public class User {
    private String id;
    
    @NotBlank(message = "密码不能为空")
	private String password;
}
PUT请求
@Test
public void whenUpdateSuccess() throws Exception {
	//用java8获取一年后的时间
    Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    System.out.println(date.getTime());
    String content = "{\"id\":\"1\", \"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
    String reuslt = mockMvc.perform(put("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8)
                                    .content(content))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value("1"))
        .andReturn().getResponse().getContentAsString();
    System.out.println(reuslt);
}
@PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user, BindingResult errors) {
    //如果有异常,打印所有异常
    if(errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user.getId());
    System.out.println(user.getUsername());
    System.out.println(user.getPassword());
    System.out.println(user.getBirthday());
    user.setId("1");
    return user;
}
DELETE请求
@Test
public void whenDeleteSuccess() throws Exception {
    mockMvc.perform(delete("/user/1")
                    .contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk());
}
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id) {
    System.out.println(id);
}
upload请求
@Test
public void whenUploadSuccess() throws Exception {
    String result = mockMvc.perform(fileUpload("/file")
                                    .file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
        .andExpect(status().isOk())
        .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}
@RestController
@RequestMapping("/file")
public class FileController {
	//文件上传路径
	private String folder = "/Users/zhailiang/Documents/my/muke/inaction/java/workspace/github/imooc-security-demo/src/main/java/com/imooc/web/controller";
	
    /**
    * 文件上传
    */
	@PostMapping
	public FileInfo upload(MultipartFile file) throws Exception {
		System.out.println(file.getName());
		System.out.println(file.getOriginalFilename());
		System.out.println(file.getSize());
		//根据路径和时间戳创建一个文件
		File localFile = new File(folder, new Date().getTime() + ".txt");
		//把内容写到文件里
		file.transferTo(localFile);
		//返回上传成功后文件的保存路径
		return localFile.getAbsolutePath();
	}
	/**
	* 文件下载
	*/
	@GetMapping("/{id}")
	public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
		try (InputStream inputStream = new FileInputStream(new File(folder, id + ".txt"));
				OutputStream outputStream = response.getOutputStream();) {
			
			response.setContentType("application/x-download");
			response.addHeader("Content-Disposition", "attachment;filename=test.txt");
			
			IOUtils.copy(inputStream, outputStream);
			outputStream.flush();
		} 
	}
}
JsonView
- 使用这个注解的场景:当查询多个用户时不希望给前台显示密码,而在查询用户详情时需要展示密码
- 使用步骤:
- 使用接口来声明多个视图
public class User {
	
	public interface UserSimpleView {};
	public interface UserDetailView extends UserSimpleView {};
    
    @JsonView(UserSimpleView.class)
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	@JsonView(UserDetailView.class)
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}
- 在值对象的get方法上指定视图
- 在Controller方法上指定视图
//{id}:id可以是字符串,也可以是数字
//@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
//正则表达式,接收的参数必须为数字:
@GetMapping("/user/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@PathVariable String id) {
    User user = new User();
    user.setUsername("tom");
    return user;
}
自定义校验注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
 * @author zhailiang
 *
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
	
	String message();
	Class<?>[] groups() default { };
	Class<? extends Payload>[] payload() default { };
}
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;
import com.imooc.service.HelloService;
/**
 * @author zhailiang
 *
 */
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
	//@Autowired
	//private HelloService helloService;
	
	@Override
	public void initialize(MyConstraint constraintAnnotation) {
        //初始化
		System.out.println("my validator init");
	}
	@Override
	public boolean isValid(Object value, ConstraintValidatorContext context) {
		//helloService.greeting("tom");
		//System.out.println(value);
        //执行校验逻辑,返回true为校验通过
		return true;
	}
}
@MyConstraint(message = "这是一个测试")
private String username;
控制器异常处理器
public class UserNotExistException extends RuntimeException {
	/**
	 * 
	 */
	private static final long serialVersionUID = -6112780192479692859L;
	
	private String id;
	
	public UserNotExistException(String id) {
		super("user not exist");
		this.id = id;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
}
mport org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.imooc.exception.UserNotExistException;
/**
 * @author zhailiang
 *
 */
//在springboot的controller层主动抛出自定义异常时会进入这个类寻找处理方法
@ControllerAdvice
public class ControllerExceptionHandler {
	//指定自定义异常类
	@ExceptionHandler(UserNotExistException.class)
    //json形式返回
	@ResponseBody
    //服务器内部错误
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
		Map<String, Object> result = new HashMap<>();
		result.put("id", ex.getId());
		result.put("message", ex.getMessage());
		return result;
	}
}
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@ApiParam("用户id") @PathVariable String id) {
    throw new RuntimeException("user not exist");
    System.out.println("进入getInfo服务");
    User user = new User();
    user.setUsername("tom");
    return user;
}
RESTful拦截
过滤器
- 使用springboot配置类来指定过滤器来拦截哪些URL,缺点是只能拿到request和response
/**
 * 
 */
package com.imooc.web.filter;
import java.io.IOException;
import java.util.Date;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
 * @author zhailiang
 *
 */
//@Component不把过滤器声明为组件,用配置类去加载过滤器
public class TimeFilter implements Filter {
	/* (non-Javadoc)
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
		System.out.println("time filter destroy");
	}
	/* (non-Javadoc)
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("time filter start");
		long start = new Date().getTime();
		chain.doFilter(request, response);
		System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
		System.out.println("time filter finish");
	}
	/* (non-Javadoc)
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(FilterConfig arg0) throws ServletException {
		System.out.println("time filter init");
	}
}
@Configuration
public class WebConfig{
	@Bean
	public FilterRegistrationBean timeFilter() {
		
		FilterRegistrationBean registrationBean = new FilterRegistrationBean();
		
		TimeFilter timeFilter = new TimeFilter();
		registrationBean.setFilter(timeFilter);
		
		List<String> urls = new ArrayList<>();
		urls.add("/*");
		registrationBean.setUrlPatterns(urls);
		
		return registrationBean;
		
	}
}
拦截器
- 缺点是不能获取到拦截方法的参数列表
@Component
public class TimeInterceptor implements HandlerInterceptor {
	/* (non-Javadoc)
	 * @see 方法执行前调用
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("preHandle");
		//打印类名和方法名
		System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
		System.out.println(((HandlerMethod)handler).getMethod().getName());
		
		request.setAttribute("startTime", new Date().getTime());
		return true;
	}
	/* (non-Javadoc)
	 * 方法执行完成后调用,抛出异常时不调用
	 */
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println("postHandle");
		Long start = (Long) request.getAttribute("startTime");
		System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
	}
	/* (non-Javadoc)
	 * @see 无论方法是否抛出错误都会执行
	 */
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		System.out.println("afterCompletion");
		Long start = (Long) request.getAttribute("startTime");
		System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
		System.out.println("ex is "+ex);
	}
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
	
	@Autowired
	private TimeInterceptor timeInterceptor;
	
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(timeInterceptor);
	}
}
切面
- 可以获取到拦截方法的参数列表
@Aspect
@Component
public class TimeAspect {
	
	@Around("execution(* com.imooc.web.controller.UserController.*(..))")
	public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
		
		System.out.println("time aspect start");
		//获取参数列表
		Object[] args = pjp.getArgs();
		for (Object arg : args) {
			System.out.println("arg is "+arg);
		}
		
		long start = new Date().getTime();
		//controller中拦截方法的返回值类型
		Object object = pjp.proceed();
		
		System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));
		
		System.out.println("time aspect end");
		
		return object;
	}
}
拦截顺序
- 方法进去:filter—>Interceptor—>ControllerAdvice—>Aspect—>Controller
- 方法退出:Controller—>Aspect—>ControllerAdvice—>Interceptor—>filter
处理异步请求
- 
    通常我们的程序只有一个主线程,当并发量增大后性能就会下降 
- 
    DeferredResult:处理异步请求,使用多线程提高REST服务性能 
- 
    当一个请求到达API接口,如果该API接口的return返回值是DeferredResult,在没有超时或者DeferredResult对象设置setResult时,接口不会返回,但是Servlet容器线程会结束,DeferredResult另起线程来进行结果处理(即这种操作提升了服务短时间的吞吐能力),并setResult,如此以来这个请求不会占用服务连接池太久,如果超时或设置setResult,接口会立即返回 
- 
    使用DeferredResult的流程: - 
        - 浏览器发起异步请求
- 请求到达服务端被挂起
- 向浏览器进行响应,分为两种情况:
3.1 调用DeferredResult.setResult(),请求被唤醒,返回结果 3.2 超时,返回一个你设定的结果
- 浏览得到响应,再次重复1,处理此次响应结果
 
 
- 
        
import org.springframework.web.context.request.async.DeferredResult;
@RestController
public class AsyncController {
	//消息队列
	@Autowired
	private MockQueue mockQueue;
	//自定义异步请求处理器
	@Autowired
	private DeferredResultHolder deferredResultHolder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	@RequestMapping("/order")
	public DeferredResult<String> order() throws Exception {
		logger.info("主线程开始");
		//生成一个订单号
		String orderNumber = RandomStringUtils.randomNumeric(8);
        //放入消息队列
		mockQueue.setPlaceOrder(orderNumber);
		//创建一个异步请求处理结果
		DeferredResult<String> result = new DeferredResult<>();
		deferredResultHolder.getMap().put(orderNumber, result);
		
		return result;
		
	}
}
/**
 * 
 */
package com.imooc.web.async;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
 * @author zhailiang
 *模拟消息队列
 */
@Component
public class MockQueue {
	//下单消息
	private String placeOrder;
	//完成消息
	private String completeOrder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	public String getPlaceOrder() {
		return placeOrder;
	}
	public void setPlaceOrder(String placeOrder) throws Exception {
		new Thread(() -> {
			logger.info("接到下单请求, " + placeOrder);
			try {
                //代表开始处理订单任务
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
            //订单任务处理完成
			this.completeOrder = placeOrder;
			logger.info("下单请求处理完毕," + placeOrder);
		}).start();
	}
	public String getCompleteOrder() {
		return completeOrder;
	}
	public void setCompleteOrder(String completeOrder) {
		this.completeOrder = completeOrder;
	}
}
/**
 * 
 */
package com.imooc.web.async;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;
/**
 * @author zhailiang
 *两个线程之间传递消息
 */
@Component
public class DeferredResultHolder {
	//key:字符串
    //value:异步消息结果
	private Map<String, DeferredResult<String>> map = new HashMap<String, DeferredResult<String>>();
	public Map<String, DeferredResult<String>> getMap() {
		return map;
	}
	public void setMap(Map<String, DeferredResult<String>> map) {
		this.map = map;
	}
	
}
/**
 * 
 */
package com.imooc.web.async;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
/**
 * @author zhailiang
 *消息队列监听器
 */
@Component
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {
	@Autowired
	private MockQueue mockQueue;
	@Autowired
	private DeferredResultHolder deferredResultHolder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		new Thread(() -> {
			while (true) {
				//无限循环,消息队列中有值就开始处理,没值等待
				if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
					
					String orderNumber = mockQueue.getCompleteOrder();
					logger.info("返回订单处理结果:"+orderNumber);
                    //设置异步消息结果
					deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
					mockQueue.setCompleteOrder(null);
					
				}else{
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
	}
}
访问/order
结果:
主线程:AsyncController:主线程开始
Thread-10:MockQueue:接到下单请求,43692617
主线程:AsyncController:主线程返回
Thread-10:MockQueue:下单请求处理完毕,43692617
Thread-7:QueueListener:返回订单处理结果:43692617