# zx-security
**Repository Path**: chmodu/zx-security
## Basic Information
- **Project Name**: zx-security
- **Description**: Spring Security/Social框架 实现了社交登录/OAuth2协议/单点登录/动态权限/mvc异步处理等
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2019-11-09
- **Last Updated**: 2022-09-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
#### Spring Security
* AsyncResult.md: 关于异步处理/hibernate validation/文件上传下载/拦截器等/swagger等
* QQLogin.md: OAuth2协议的介绍和社交登录
* OAuth2.md: 用Spring Security开发自己的OAuth2协议实现,自己实现服务提供商的功能,分发给其他应用access_token
* SSO(JWT).md: 基于JWT实现单点登录
* Dynamic.md:实现了完全动态的权限控制系统.以及spring security框架的大致源码解析.
* WebFlux.md: Spring Boot 2.0 + 版本特性 WebFlux异步响应式框架入门
#### 总结
该项目实现了许多东西.
* 基于SpringSecurity框架的表单/验证码/手机验证码登录方式.
* 基于SpringSocial的社交登录(qq和微信)
* 基于SpringSecurityOAuth2的,将自己作为认证服务器和资源服务器.并使用社交/表单/验证码/手机验证码登录方式 获取access_token.
* 将SpringSecurityOAuth2扩展为JWT(json web token),并实现对应的单点登录.
* 基于SpringSecurity的角色_权限对应的完全动态的权限控制. 例如运行时修改要拦截的url等.
* Spring Boot2版本增加的WebFlux框架..入门...简陋.
* Spring MVC异步处理的实现.
#### 总结思想
框架的源码其实并不一定有多么深(不是每个框架都和Spring Contetnxt那样的......).
看源码能搞清楚很多事情.并且可以学到写轮子的技巧.(对于实际业务开发..用那么多可扩展的设计模式是作死...)
#### 关于设计模式在项目开发中运用的感想
关于设计模式,我曾经照着一篇博客,敲打过几乎常用的所有设计模式.然后在项目中真正运用得并不多.
我还记得那个博主写的一句话,最常用的是模版方法模式.我并无他想.
直到前些天,我开发公司的一个短信发送平台时,因为需要接入多个短信发送渠道,并且以后需要能扩展渠
道(这周就在新加一个渠道.移动的CMPP协议,贼难受).我就一直尽量在编写代码时将其设计的比较抽象,并尽
可能地消除了一些后期可能需要改动的if语句;例如使用表驱动(类似一个数组或map,使用索引或key选择实现
类)这样子的;
刚开始时,我并没有使用其他设计模式的想法.在我按部就班地写完第一个渠道的发送方法(无非就是获取
发送参数,封装成请求参数,发送,处理同步返回值等(当然该系统还有其他比较繁琐的逻辑)),突然就顿悟了,
稍稍重构了下代码,将其重构成了模版方法.顿时后面的代码都十分简单了.然后在异步回调,短信上行的处理中
,我全部使用了模版方法.终于体会到他的强大.简单而强大.
此外,近来我也细细想过.对于一个并无扩展需求的业务系统来说,设计模式或者说抽象的编程真的没什么
必要.就拿目前简直是行业规范的service层接口来说把,哪个业务系统会需要把整个service层的实现全部替
换了.将其抽象出接口来,简直就是给开发找麻烦.
以我目前的理解,设计模式最好的运用场合是在写轮子的时候.将应用的逻辑进行分层,将有自定义需求的
所有逻辑抽象出来,方便以后自定义的扩展才是正解.
#### bug记录
* 如果出现idea父模块无法导入子模块,可以在设置里面搜索maven,找到忽略的文件,去掉该子模块的勾选即可。
* 如果导入后,发现还是未解决问题,可以打开idea的maven project,选中该项目,刷新即可
* SpringMVC中RequestMapping("/")这个/不能乱加,如果controller类已经有/user这样一个前缀,
那么如果在方法上在注解上"/",访问的路径就会为/user/,使用/user将无法访问
* !!!之前遇到HttpClient发送json串请求controller方法,参数一直为null.是因为没有加@RequestBody.
* !!!spring boot 属性注入 @ConfigurationProperties 必须有getter/setter方法才能生效 血泪教训
* IDEA mavenProject窗口中无法刷新出新建的Maven项目,可尝试在Project窗口的项目上右击,选择刷新即可.
#### 重大发现.特大新闻.
* 一直以来.我都有一个疑问.就是为什么自己配置的@ConfigurationProperties.总是无法在application.yml文件中自动提示.显示无法解析.
于是我今天特地好好研究了一番.找了另一框架中的自定义配置,反复对比我和它的差别.无意间还点进了一个文件:
META-INF/spring-configuration-metadata.json. 我以为找到了答案.但是他的配置很繁琐,不可能
每个属性都这么配置过去.而且这个文件只有在编译后才会出现..
恍然大雾...比对(我和kafkaProperties都是嵌套的属性)..将子属性类变成父属性类的静态内部类..重新编译.
在yml中出现了自动提示. Java规范的属性名是驼峰.它默认将驼峰转换为 'xx-xx'. 但手动输入原属性名.也是可以匹配上的. END
* 而后,又出现一个问题.就是当我自定义属性上使用javadoc注解了中文注释时.在yml的自动提示中乱码了.
我的IDEA很早将几乎所有编码都设置成了UTF-8,在metadata.json中,也是UTF-8,并且其他所有地方都不存在乱码.
于是.海里捞针的百度..终于发现了问题
> 在idea目录/bin中的idea64.exe.vmoptions中,追加-Dfile.encoding=UTF-8,重启IDEA即可.
* 而后..他突然莫名其妙又提示无法解析..重新编译.重启IDEA等都试了..打开那个json文件看了下..
果然,是文件中没有生成.于是乎..先clean.再重新编译.ok.
#### 奇淫巧技
* Spring Boot启动类自动扫描的包,是他当前所处的包下面所有的类.所以才会有位置的限制.也可以用@Scan啥的注解自定义
* 如下打包时,可以将所有依赖都打到jar中去运行.基于springboot
>
org.springframework.boot
spring-boot-maven-plugin
repackage
>
* maven打包时,必须将自己依赖的其他子模块都先install.
* 在yml中如下配置session超时时间,单位为秒.但是如果.小于60s,默认也会转为1分钟.
>
server:
session:
timeout: 10
>
* StringUtils.substringBetween() 截取字符串中,被哪两个首位字符包裹的字符串
* ConditionalOnProperty, 根据配置的属性和属性值,判断是否启用bean
>
@ConditionalOnProperty(prefix = "zx.security.oauth2",//属性前缀
name = "storeType",//属性
havingValue = "jwt",//预期值
matchIfMissing = true//指定属性如果没有设置,条件是否匹配,为true,则没有设置属性时,注入bean
//注意,是属性是否设置,而不是是否是该值.如果没设置,注入该bean
)
>
* PasswordEncoder类,可以直接用来加密解密
* ServletWebRequest类,可以封装request和response.
* 如下写法,可以将spring容器中所有该类型的bean都放入map中,并以每个bean各自的name为key:
>
@Auwired
private Map userMap;
>
* 如下写法,可以用来自定义配置bean:
写轮子的时候用,如果调用者没有配置自己的bean,才使用轮子默认的bean,除了name外,还可以使用type等
>
//该注解表示将在该类中使用@Bean配置bean,相当于用java代码写原先的spring.xml中的beans标签配置bean
@Configuration
public class CaptchaBeanConfig {
@Autowired
private SecurityProperties securityProperties;
//当spring在容器中无法找到名字为imageCaptchaGenerator的bean的时候,才使用该方法生成bean
@Bean
@ConditionalOnMissingBean(name = "imageCaptchaGenerator")
public CaptchaGenerator imageCaptchaGenerator() {
//创建默认的图片验证码生成器
BasicImageCaptchaGenerator imageCaptchaGenerator = new BasicImageCaptchaGenerator();
imageCaptchaGenerator.setSecurityProperties(securityProperties);
return imageCaptchaGenerator;
}
}
>
* 实现InitializingBean,并重写方法,可以在所有bean初始化完毕时,执行某些操作
* ServletRequestUtils工具.如下方法可以从request获取指定类型的指定key的参数值,如果取不到就用默认值:
>
ServletRequestUtils.getIntParameter(
request.getRequest(),
"width",
securityProperties.getCaptcha().getImage().getWidth());
>
* 在resources目录中,新建resources目录.并写一个index.html.可以访问url/index.html直接访问到
* ObjectMapper : springMVC在启动时自动注册的bean,用于将对象转为json,可以直接注入到代码中
* ctrl + h ,类的继承图;
* 使用MediaType(spring的),几乎有所有常用的http请求的contentType属性的值的常量:
例如MediaType.APPLICATION_JSON_UTF8_VALUE
* 使用TimeUnit.SECONDS.sleep(x);可以使用秒..暂停线程.
* 在idea的MavenProject窗口选择module,右击选择show dependencies,可以很清楚地查看依赖关系图
* 如下,可以获取classpath目录的路径(该路径为classes下的resources路径):
ClassPathResource resource = new ClassPathResource("mock/response/01.json");
resource.getFile();
* Commons-lang包中有RandomStringUtils可以生成随机数
* 在@ControllerAdvice注解的类中的@ExceptionHandle注解的方法上,可以使用如下注解返回指定状态码:
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
* JsonPath:在github中的一个项目,可以用类似jquery选择器那样的语法,取出一个json中任意的信息。
* 在spring mvc中使用url传参时,可以指定参数的正则表达式(如下就是指定id必须为数字):
@GetMapping("/user/{id:\\d+}")
注意,如果传参不符合该正则,返回的状态码将是404
* 使用如下代码,可以在不修改实体类的情况下,反射实体类的toString方法:
ReflectionToStringBuilder.toString(userQueryCondition, ToStringStyle.MULTI_LINE_STYLE);
直接sout上面的表达式即可输出;
* 如果在controller中使用springDataJpa的pageable对象接收分页参数,可以用如下注解,来指定其默认值:
@PageableDefault(size = 10,page = 0,sort = "username,asc") Pageable pageable
* 使用set集合,如果有排重需求,并且使用的不是java基本类型,最好
重写hashcode和equals方法.IDEA可以自动生成这两个方法.
生成时可以指定比较哪些字段进行排重.一般只比较id即可.
* decimal(20,2): 该类型表示的是18位整形和2位小数.注意..
* 使用Decimal类进行计算时.如果构造函数传入的是Double之类的类型,
一样无法保证精度.只有使用String类型的值构造(可以Double.toString).
然后进行计算才能保证精度.
#### 搭建环境
1. 新建项目zx-security
2. 在该项目下,建立四个子模块
* app
* browser
* core
* demo
3. 在zx-security该父模块下添加依赖:
>
io.spring.platform
platform-bom
Brussels-SR4
pom
import
org.springframework.cloud
spring-cloud-dependencies
Dalston.SR2
pom
import
>
和编译插件:
>
org.apache.maven.plugins
maven-compiler-plugin
2.3.2
1.8
1.8
UTF-8
>
4. 在zx-security-core子模块中增加依赖(自行查看)
5. 然后在app模块中加入core模块的依赖:
>
com.zx.security
zx-security-core
${zx.security.version}
>
注意,该version是写在父模块中的,可直接引用
6. 在browser中加入依赖:
>
com.zx.security
zx-security-core
${zx.security.version}
org.springframework.session
spring-session
>
7. 在demo中依赖:
>
com.zx.security
zx-security-browser
${zx.security.version}
>
#### Hello World
1. 在demo中新建com.zx包,然后新建DemoApplication类,并加入@SpringBootApplication注解等。
写到这的时候,我才意识到,这就是springBoot项目不依赖spring为父模块,而是依赖自定义的父模块的写法。
2. 直接在该Application类上写@RestController,然后写个/test
3. 新建application.yml,配置如下(如果不关闭security,默认用户名为user,密码为启动时输出的一串默认密码):
>
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/zx-security?useUnicode=yes&characterEncode=UTF-8
username: root
password: 123456
session:
store-type: none #暂时关闭spring-session的配置,不然会报错
security:
basic:
enabled: false #暂时关闭spring-security的配置,不然会报错
>
4. 注意,此时如果将demo项目打包,只会是一个普通的jar,无法直接运行启动spring boot,
还需要在pom.xml中加入,才可以将它包含的依赖一起打包,以便直接用jar运行spring boot,
包名就是finalName:
>
org.springframework.boot
spring-boot-maven-plugin
repackage
Demo
>
#### Restful
* 用URL描述资源
* 用HTTP方法描述行为
* 用HTTP状态码表示不同的结果
* 用json交互数据
* 资源表达中包含了链接信息,也就是返回的json中包含其他的api url(一般达不到该要求)
例如:
>
/user?name=zx GET 查名字
/user/1 GET 查id
/user POST 增
/user/1 DELETE 删
/user/1 PUT 改
>
#### Web层测试用例编写
1. 在demo项目引入依赖:
>
org.springframework.boot
spring-boot-starter-test
>
2. 编写如下测试类即可测试controller的代码:
>
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
//spring的web容器
@Autowired
private WebApplicationContext wac;
//mvc模拟类
private MockMvc mockMvc;
//使用容器构建mvc模拟类
@Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void whenQuerySuccess() throws Exception {
/**
* 向/user发起get请求,其中header的contentType为json,参数自定义
* 并且期望,返回的http状态码为200,
* 并且期望,将返回的json解析后,数组长度为3
*/
mockMvc.perform(MockMvcRequestBuilders.get("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.param("username", "zx"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
}
>
#### @JsonView使用-注意,指定视图后,对象所有属性都需指定视图,否则将不会返回
1. 将要返回成json串的实体类,进行改造:
创建不同的视图,每个视图代表一个controller的方法,区分每个方法返回时,该对象该返回的属性和不返回的属性.
以往我的写法都是创建多个返回的dto对象,显得很_麻瓜
>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//用户简单视图
public interface UserSimpleView {};
//用户详情视图
public interface UserDetailView extends UserSimpleView {};
//将该属性在简单视图展示
@JsonView(UserSimpleView.class)
private String username;
//将密码属性在详情视图才展示,
// 但是详情视图仍然会显示简单视图的属性,因为有一个继承关系
@JsonView(UserDetailView.class)
private String password;
}
>
2. 在controller层的方法上增加如下注解,即可返回指定视图:
@JsonView(User.UserSimpleView.class)
#### SpringSecurity基本原理

* 过滤器链
* 身份验证过滤器(任意一种该过滤器通过后即可),可通过配置决定某一过滤器是否生效(其他过滤器则不行)
* UsernamePasswordAuthenticationFilter-处理表单登录
* BasicAuthenticationFilter-处理basic(最原始的弹出框)登录
* ExceptionTranslationFilter-捕获FilterSecurityInterceptor抛出的异常,作相应处理
* 最后一个过滤器(之后就是真正的方法了)
* FilterSecurityInterceptor-真正决定当前请求能否访问
#### SpringSecurity表单验证demo-以上的基本都是demo模块,往下开始在browser模块
1. 在browser模块中新建BrowserSecurityConfig,如下:
>
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .httpBasic()//最原始的弹出框登录,二选一
.formLogin()//表单页面登录,二选一
.and()
.authorizeRequests()//进行验证配置
.anyRequest()//任何请求
.authenticated();//都需验证
}
}
>
2. 因为demo模块依赖了browser模块,所以此时再次启动demo模块
3. 访问任何url,都会进入一个表单验证界面,需要输入帐号密码才可
* 用户信息获取
* 重写UserDetailsService接口,从数据库中加载用户信息及权限
* 处理用户逻辑
* 在UserDetailsService接口中返回的UserDetails接口对象
有几个校验方法
* isAccountNonExpired()是否没有过期
* isCredentialsNonExpired()认证(密码)是否过期
* isAccountNonLocked()是否没有锁定
* isEnabled()是否可用/被删除
* 在构造User对象时,可以传入上面这些校验方法的boolean值
* 可以自定义实现UserDetails接口
* 处理密码加密解密
* 使用PasswordEncoder类(crypto包中的)
* encode()加密密码;该方法需要我们在存入密码时调用
* match()判断加密后密码是否匹配;security框架自行调用
* 在BrowserSecurityConfig类中配置一个bean,返回PasswordEncoder,
使用已有的BCryptPasswordEncoder类,其BCrypt加密比MD5安全,但性能稍低:
>
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
>
* 该BCryptPasswordEncoder类极其强大,即使是相同密码,每次生成的密文都不相同,
因为每个密文中还携带了不同的盐salt;
4. BrowserSecurityConfig类配置:
>
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .httpBasic()//最原始的弹出框登录,二选一
.formLogin()//表单页面登录,二选一
.loginPage("/login.html")//登录页面url
.loginProcessingUrl("/login")//登录方法url,默认就是/login,用post方法
.and()
.authorizeRequests()//进行验证配置
.antMatchers("/login.html")//匹配这些路径
.permitAll()//全部允许
.anyRequest()//任何请求
.authenticated();//都需验证
http.csrf().disable();//暂时关闭csrf,防止跨域请求的防护关闭
}
>
5. 配置BrowserSecurityController,也就是进入登录页面的方法逻辑:
>
/**
* 当访问的页面需要验证时,security会跳转到下面这个接口的登录页面,
* 但会把真正要访问的页面,也就是跳转前的页面存到cache中
*/
private RequestCache requestCache = new HttpSessionRequestCache();
/**
* 重定向策略,用于跳转请求
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 当需要身份认证时,跳转到这里
*
* 其需求是.如果之前访问的url不是页面,就返回异常信息;如果是页面,就跳转到登录页;
* 此处是用访问的后缀是不是.html结尾来判断的,
* 我觉得比较好的是,根据请求头的context-type来判断.
* 就是这个:
* @RequestMapping(
* produces = {"text/html"}
* )
* @param request
* @return
*/
@RequestMapping("/view/login")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)//返回401,未授权状态码
public SimpleResponse requiredAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取到跳转前的请求
SavedRequest savedRequest = requestCache.getRequest(request, response);
//如果请求不为空
if (savedRequest != null) {
//获取到请求url
String target = savedRequest.getRedirectUrl();
log.info("引发跳转的请求是:{}", target);
//如果该请求是.html结尾的,跳转到登录页,否则表示不是请求的页面,返回json
if (StringUtils.endsWithIgnoreCase(target, ".html")) {
//跳转到登录页,从yml配置中读取登录页路径
redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
}
>
6. 在core模块中添加配置属性bean-SecurityProperties,其中包括了BrowserProperties
>
@Data
@ConfigurationProperties(prefix = "zx.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
}
@Data
public class BrowserProperties {
//登录页配置-默认值
private String loginPage = "/login.html";
}
>
这样,配置在demo模块的yml中的如下就会被读取到SecurityProperties类的browser中的loginPage中
>
zx:
security:
browser:
loginPage: /login1.html
>
然后再配置一个(我他妈从来没配置过?????!!!!),让上面的属性读取类生效
(然后将这个loginPage属性注入到BrowserSecurityController还有BrowserSecurityConfig中去)
>
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
>
7. 修改登录成功后的处理方式:
自定义身份验证成功处理类
>
/**
* author:ZhengXing
* datetime:2017-11-24 20:40
* 自定义身份验证成功处理器
* security默认在验证成功后跳转到此前访问的页面,但是如果前端的登录是
* ajax方式的,不适合跳转页面,所以需要更改成功后的处理
*/
@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
/**
* springMVC在启动时自动注册的bean,用于将对象转为json
*/
@Autowired
private ObjectMapper objectMapper;
/**
* 当登陆成功时
* @param request
* @param response
* @param authentication 封装了认证信息
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("登录成功");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//将authentication对象转为jsonString,返回
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
>
在BrowserSecurityConfig配置处理类:
>
.successHandler(customAuthenticationSuccessHandler)//配置验证成功处理器
>
如下是handler的方法中的authentication对象的一些属性:
>
{
authorities: [
{
authority: "admin"
}
],
details: {
remoteAddress: "127.0.0.1",
sessionId: "24AC42B2747541800EB5C4744AF2CEF0"
},
authenticated: true,
principal: {
password: null,
username: "aaa",
authorities: [
{
authority: "admin"
}
],
accountNonExpired: true,
accountNonLocked: true,
credentialsNonExpired: true,
enabled: true
},
credentials: null,
name: "aaa"
}
>
8. 修改登录失败后的处理方式:
自定义处理器:
>
/**
* author:ZhengXing
* datetime:2017-11-24 21:03
* 自定义身份验证失败处理器
*/
@Component("customAuthenticationFailHandler")
@Slf4j
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* 在异常中,有错误消息,是关于为什么登录失败的
*
* 默认登录失败是跳转到一个登录失败的url,此处改了处理方式
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("登录失败");
//状态码500
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//将authentication对象转为jsonString,返回
response.getWriter().write(objectMapper.writeValueAsString(e));
}
>
配置处理器:
>
.failureHandler(customAuthenticationFailHandler)//配置验证失败处理器
>
在处理器的异常中,有对应的失败消息
9. 将使用默认的重定向还是使用自定义的json返回处理方式加入配置属性,可自定义配置
增加登录类型枚举,并在BrowserProperties属性类中增加对应属性:
>
public enum LoginType {
//重定向
REDIRECT,
//返回json
JSON,
;
}
>
然后在成功和失败处理器中,都将原来的实现成功失败处理接口,改为继承security默认的处理器实现类,然后重写对应方法:
根据类型选择自定义实现还是使用父类默认的方法
>
//如果配置的的登录方式是json,使用自定义处理器
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//将authentication对象转为jsonString,返回
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else{
//否则使用父类处理方法,重定向
super.onAuthenticationSuccess(request,response,authentication);
}
>
如下配置即可:
>
zx:
security:
browser:
#配置登录方式
loginType: REDIRECT
>
#### 认证流程源码级详解
* 点击登录,进入UsernamePasswordAuthenticationFilter类
* 在该类的attemptAuthentication()方法中获取到请求的用户名密码
* 用用户名密码构建了UsernamePasswordAuthenticationToken对象
* 该对象是Authentication接口的实现.
* ...下次有时间自己看吧
* 对于如何在多个对象间共享用户认证信息,用的是SecurityContextHolder,
其本质还是ThreadLocal
#### 在Controller中获取登录用户的信息
>
/**
* 获取用户信息
*/
@GetMapping("/me")
public Object getCurrentUser(@AuthenticationPrincipal UserDetails user) {
//一种方法是自己获取
//SecurityContextHolder.getContext().getAuthentication()
//第二种方式
//直接在方法参数中写Authentication,即可获取
//第三种,只想获取Authentication中的UserDetails
//在方法参数中这么写@AuthenticationPrincipal UserDetails user
return user;
}
>
#### 从session中获取用户信息
* 存储在session中的SPRING_SECURITY_CONTEXT 这个key中.
* Session.SPRING_SECURITY_CONTEXT.authentication.principal.username
#### 扩展UserDetails
* 实现如下类,然后在自定义的UserDetailService中新建该类即可
* 注意,在获取时可以使用 @AuthenticationPrincipal CustomUser user, 在方法上直接使用自定义的类来接收
>
/**
* author:ZhengXing
* datetime:2017/12/12 0012 12:08
* 自定义用户类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CustomUser implements UserDetails {
private Long id;
private String username;
private String password;
private Boolean enabled;
private Collection extends GrantedAuthority> authorities;
public CustomUser(Long id, String username, String password, Boolean enabled) {
this.id = id;
this.username = username;
this.password = password;
this.enabled = enabled;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
>
#### 图形验证码
* 新建Captcha类,保存验证码的图片流/code/过期时间
* 完成captchaController,生成图片并返回
* 在securityConfig配置类中,允许未登录便访问获取验证码的路径
* 自定义CaptchaFilter过滤器,并将其加入security配置中
* 重构
* 将验证码的大小/字符数/需要验证的url/生成方法都变成可配置的
#### Remember记住我功能
* 基本原理
* 在UsernamePasswordAuthenticationFilter认证成功后,
调用RememberService的TokenRepository,将token生成并写入cookie和数据库
* 用户下次访问时,访问到RememberMeAuthenticationFilter,
读取到cookie中的token,从数据库中查询到对应token的用户名,
然后使用UserDetailsService找到用户信息,完成登录验证
* 在登录页面中增加记住我的选择,使用remember-me为name:
* 在BrowserSecurityConfig类中如下配置:
Bean:
>
@Autowired
private DataSource dataSource;
//记住我功能的配置,需要注入
@Autowired
private UserDetailsService customUserDetailsService;
/**
* 记住我功能
* 生成用来将token写入数据库的PersistentTokenRepository类
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//设置在启动时,创建对应的数据库中存储token的表
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
>
配置:
>
.and()
.rememberMe()//配置记住我功能
//token仓库配置,用来将token存入数据库
.tokenRepository(persistentTokenRepository())
//token过期秒数配置
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
//查询用户信息的service
.userDetailsService(customUserDetailsService)
>
* 启动后,会自动在数据库中创建persistent_logins表
* 然后登录,此时,该表中会多一条记录,保存了该用户的用户名/session/token/最后登陆时间;
然后我们关闭程序,重新启动程序,此时,之前登录的session应该无效了;
直接访问需要验证的url,会发现已经无需登录了
#### 短信验证码登录-security框架外的东西(core-com.zx.security.validate)
* 完成除security框架外的代码后,目前是这个架构:
* 在controller中有两个方法,分别实现图形/短信验证码
* 有图形/短信验证码生成器接口和两个实现类
* 图形验证码需要用流输出回去,短信验证码需要调用短信验证码发送器接口的方法.
* 可使用模版方法重构为如下逻辑(我真的懒得重构了(...最终还是重构了,难受))
* controller中只有一个方法,使用url传参,得到验证码类型是短信还是图形
* 定义了一个验证码处理接口和验证码处理接口抽象类
* 实现了 生成验证码 -> 存储验证码 -> 发送验证码/返回图片流 整个的模版方法,每一步不同的划分为各自的抽象方法
* 两个实现抽象类的实现类,分别完成各自不同的方法就可以了,
* 图片验证码返回流
* 短信验证码发送短信
* 短信验证码流程

#### 短信验证码登录-扩展security框架(core-com.zx.security.authentication.mobile)
* 实现自己的SmsCaptchaAuthenticationToken类(Authentication的子类):
用于在未认证通过时存放手机号,通过时,存放用户信息
* 实现SmsCaptchaAuthenticationFilter类,
用于从请求中获取手机号,其他关于请求的详细信息,将其存入SmsCaptchaAuthenticationToken等
* 实现SmsCaptchaAuthenticationProvider类,
使用userDetailsService.loadUserByUsername验证其身份,设定自己这个Provider支持哪些Authentication,
如果身份验证通过了,将其用户信息和之前的详细信息,存到新的SmsCaptchaAuthenticationToken中,返回
* 实现SmsCaptchaFilter过滤器,来在最开始验证短信验证码是否正确,基本就复制图形验证码的过滤器,稍微改下即可
* 在core中新建SmsCaptchaAuthenticationSecurityConfig,配置上面的实现类;
并在BrowserSecurityConfig中,同样配置SmsCaptchaFilter;
并在BrowserSecurityConfig中如下配置:
>
.and()
.apply(smsCaptchaAuthenticationSecurityConfig);
>
* 全部实现后,可重构的包括两个类似的验证码过滤器,需要相同引用的变量抽成常量,
将security配置类分为app配置/web配置/验证码配置等各个类,放到各自模块..
这些我是真的懒得打了...
#### Session
* 处理session失效
>
在配置类中如下配置
.and()
.sessionManagement()
.invalidSessionUrl("/session/invalid")//session失效后跳转到的路径
// .invalidSessionStrategy(InvalidSessionStrategy)//可以自定义session失效时的策略
.and()
>
* 处理session并发登录
>
在配置类中如下配置
.and()
.sessionManagement()
.invalidSessionUrl("/session/invalid")//session失效后跳转到的路径
//.invalidSessionStrategy(InvalidSessionStrategy)//可以自定义session失效时的策略
.maximumSessions(1)//同一session同一时间最大数量,一般就是1,也就是不同机器登录会被挤下线
.maxSessionsPreventsLogin(true)//该参数表示,当session并发到达最大值后,不允许后来者再登录
// .expiredUrl("xxx")//session被挤下线后跳转的url
.expiredSessionStrategy(customExpiredSessionStrategy)//被挤下线后的自定义策略
.and()//这个and返回SessionManagementConfigurer
.and()//这个and才返回原配置类
自定义策略类自己看.就是返回了个json.也可以直接重定向到一个url.
最终效果就是.用户异地再登录后,前一个用户再次访问,就执行了自定义策略.
或者就是上面配置类中的,并发上限达到后,不允许后来者再登录.
>
* 集群时的session共享.用spring-session和redis实现
导入spring-session依赖后,如下即可.
>
spring:
session:
store-type: redis #可使用none,暂时关闭spring-session的配置
redis:
host: 106.14.7.29
password: 123456
>
* 注销处理
>
注销的默认请求路径是 /logout
注销时security的处理逻辑
1. 使当前session失效
2. 清除remember-me记录
3. 清空当前的EecurityContext
4. 重定向到登录页并携带一个logout的空参数
如下配置即可
.logout()
.logoutUrl("/logout")//请求注销的url,默认是/logout
.logoutSuccessUrl("/logout.html")//注销成功后跳转到的路径
// .logoutSuccessHandler()//自定义注销成功后的处理逻辑
// .deleteCookies("")//注销时可删除指定key的cookies
.and()
>