0. 概述
- AbstractChannelSecurityConfig中http.formLogin()启用form表单登录
- 使用默认的DaoAuthenticationProvider实现用户名密码登录
- 自己实现UserService
UserService加了@Component注解,SpringSecurity会自动扫描加载他
1. 起步
1.1 扩展WebSecurityConfigurerAdapter类
配置路由、密码加密类等
package com.wxm.spring.security.browser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import net.bytebuddy.implementation.bind.annotation.Super;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private MyUserDetailsService userDetailsService;
//密码加密类
//MyUserDetailsService 中会通过@Autowired引入
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登录
.loginPage("/imooc-signIn.html") //指定登录页面,替代spring security默认页面
.loginProcessingUrl("/authentication/form") //对应form表单中的action,UsernamePasswordAuthenticationFilter将处理此URL对应的请求
//http.httpBasic() //http登录对话框
.and()
.authorizeRequests()
.antMatchers("/imooc-signIn.html").permitAll() //别忘了,不然会循环跳转
.anyRequest()
.authenticated()
.and()
.csrf().disable(); //关闭CSRF验证
}
/*
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//inMemoryAuthentication 从内存中获取
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user1")
.password(new BCryptPasswordEncoder()
.encode("123456"))
.roles("USER");
}
*/
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider
= new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
}
1.2. 实现UserDetailsService 接口
loadUserByUsername根据用户名查找并返回用户信息
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
//@Autowired
//private Mapper
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查找用户信息
User user;
//user = new User(username,new BCryptPasswordEncoder().encode("1"),AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
logger.info("根据用户名查找用户信息 "+username);
/*
user = new User(username, new BCryptPasswordEncoder().encode("1"),
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
*/
String password = passwordEncoder.encode("1");
logger.info("数据库密码是: "+password);
user = new User(username, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
logger.info("根据查找到的用户信息判断该用户是否被冻结");
return user;
}
}
1.3. 创建/main/java/resources/resources/imooc-signIn.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>imooc-signIn.html</h2>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
2. 配置项管理
涉及内容:
2.1 wxm-spring-security-demo
2.1.1 application.properties
/main/java/resources/application.properties:
#自定义登录页,替换默认登录页,由SecurityProperties读取和管理
imooc.security.browser.loginPage = /demo-signIn.html
2.1.2 demo-signIn.html
/main/java/resources/resources/demo-signIn.html
2.2 wxm-spring-security-browser
2.2.1 BrowserSecurityConfig
- 配置路由
- 设置密码加密类
package com.wxm.spring.security.browser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.wxm.spring.security.core.properties.SecurityProperties;
import net.bytebuddy.implementation.bind.annotation.Super;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private SecurityProperties securityProperties;
//密码加密类
//MyUserDetailsService 中会通过@Autowired引入
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
http.formLogin() //表单登录
.loginPage("/imooc-signIn.html") //指定登录页面,替代spring security默认页面
.loginProcessingUrl("/authentication/form") //对应form表单中的action,UsernamePasswordAuthenticationFilter将处理此URL对应的请求
//http.httpBasic() //http登录对话框
.and()
.authorizeRequests()
.antMatchers("/imooc-signIn.html").permitAll() //别忘了,不然会循环跳转
//.antMatchers("/authentication/form").permitAll() //别忘了,不然会循环跳转
.anyRequest()
.authenticated()
.and()
.csrf().disable();
*/
/*
http.formLogin() //表单登录
.loginPage("/authentication/require") //跳转到BrowserSecurityController对应路径
.loginProcessingUrl("/authentication/form") //对应form表单中的action,UsernamePasswordAuthenticationFilter将处理此URL对应的请求
//http.httpBasic() //http登录对话框
.and()
.authorizeRequests()
.antMatchers("/authentication/require").permitAll() //别忘了,不然会循环跳转
.anyRequest()
.authenticated()
.and()
.csrf().disable();
*/
http.formLogin() //表单登录
.loginPage("/authentication/require") //跳转到BrowserSecurityController对应路径
.loginProcessingUrl("/authentication/form") //对应form表单中的action,UsernamePasswordAuthenticationFilter将处理此URL对应的请求
//http.httpBasic() //http登录对话框
.and()
.authorizeRequests()
.antMatchers("/authentication/require"
,securityProperties.getBrowser().getLoginPage()).permitAll() //别忘了,不然会循环跳转
//此处securityProperties.getBrowser().getLoginPage()读出的就是wxm-spring-security-demo//main/java/resources/application.properties中的"imooc.security.browser.loginPage = /demo-signIn.html"
//.antMatchers("/authentication/form").permitAll() //别忘了,不然会循环跳转
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
/*
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//inMemoryAuthentication 从内存中获取
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user1")
.password(new BCryptPasswordEncoder()
.encode("123456"))
.roles("USER");
}
*/
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider
= new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
}
2.2.2 BrowserSecurityController
自定义用户登录校验跳转逻辑
package com.wxm.spring.security.browser;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.wxm.spring.security.browser.support.SimpleResponse;
import com.wxm.spring.security.core.properties.SecurityProperties;
import org.springframework.security.web.RedirectStrategy;
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
//用于获取引发跳转的请求,请求缓存器
private RequestCache requestCache = new HttpSessionRequestCache();
//用于请求跳转
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties SecurityProperties;
/**
* 当需要身份认证时,跳转到这里
* @param request
* @param response
* @return
* @throws IOException
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED) //401
public SimpleResponse requireAuthentication(HttpServletRequest request,HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if(savedRequest!=null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是: "+targetUrl);
//请求路径以.html代表来源是浏览器,否则来源是客户端调用接口,例如:http://localhost/user/1
if(StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
//跳转到application.properties中配置的登录页路径,会直接跳转走,后续代码不会继续执行
redirectStrategy.sendRedirect(request, response, SecurityProperties.getBrowser().getLoginPage());
}
}
//不是来源于.html的浏览器请求,那么就是来源于客户端,返回一个字符串提示
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
}
}
2.2.3 MyUserDetailsService
loadUserByUsername根据用户名查找用户信息返回User(implements UserDetails)
package com.wxm.spring.security.browser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
//@Autowired
//private Mapper
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查找用户信息
User user;
//user = new User(username,new BCryptPasswordEncoder().encode("1"),AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
logger.info("根据用户名查找用户信息 "+username);
/*
user = new User(username, new BCryptPasswordEncoder().encode("1"),
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
*/
String password = passwordEncoder.encode("1");
logger.info("数据库密码是: "+password);
user = new User(username, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
logger.info("根据查找到的用户信息判断该用户是否被冻结");
return user;
}
}
2.2.4 SimpleResponse
一个简单的封装类,用于BrowserSecurityController的请求应答信息封
package com.wxm.spring.security.browser.support;
public class SimpleResponse {
private Object contentObject;
public Object getContentObject() {
return contentObject;
}
public void setContentObject(Object contentObject) {
this.contentObject = contentObject;
}
public SimpleResponse(Object contentObject) {
super();
this.contentObject = contentObject;
}
}
2.2.5 imooc-signIn.html
默认登录页面
/main/java/resources/resources/imooc-signIn.html
2.3 wxm-spring-security-core
2.3.1 SecurityCoreConfig
加载SecurityProperties,读取各个子模块中application.properties中的配置项
package com.wxm.spring.security.core;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import com.wxm.spring.security.core.properties.SecurityProperties;
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
//使配置读取器SecurityProperties生效
public class SecurityCoreConfig {
}
2.3.1 SecurityProperties
顶级类,管理所有配置项,调用BrowserProperties等,读取各个子模块中application.properties中的配置项
package com.wxm.spring.security.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
//prefix对应application.properties中的变量前缀:imooc.security.browser.loginPage = /demo-signIn.html
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
//变量名browser一定要与application.properties中的项目名对应上:imooc.security.browser.loginPage = /demo-signIn.html,不然无法自动读出来
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
2.3.1 BrowserProperties
读取wxm-spring-security-browser的application.properties中的配置项
package com.wxm.spring.security.core.properties;
/**
* @author wxm
* 读取wxm-spring-security-demo/main/java/resources/application.properties中的imooc.security.browser.loginPage = /demo-signIn.html
* 注意私有成员变量loginPage与application.properties中的配置项名称对应关系
*/
public class BrowserProperties {
private String loginPage = "/imooc-signIn.html";
//默认跳转到登录页:/imooc-signIn.html
//如果application.properties中做了配置,则跳转到定制的登录页
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
3.自定义登录成功和失败处理
AuthenticationSuccessHandler接口,返回Authentication,包含用户认证信息,根据登录方式不同(用户名密码、微信、qq等),包含的信息也不一样
AuthenticationFailureHandler接口,
4认证处理流程说明
- UsenamePasswrdAuthenticationFilter
获取并封装用户名、密码等登录信息,生成AuthenticationToken - AuthenticationManager
管理AuthenticatioinProvider,以AuthenticationToken为参数调用各个AuthenticatioinProvider - AuthenticatioinProvider
实现认证逻辑:用户名密码、微信、qq等,每个AuthenticatioinProvider根据传入的AuthenticationToken验证是否是属于自己负责的认证逻辑,如果是则进行认证校验;校验过程为:通过UserDetailService获取UserDetail,验证有效性,验证通过则创建并返回Authentication,并以Authentication为参数,调用自定义的AuthenticationSuccessHandler接口实现类,不通过则生成对应异常,并以该异常为参数,调用自定义的AuthenticationFailureHandler接口实现类 - UserDetailService
- UserDetail
- Authentication
5认证处理结果如何在多个请求之间共享
AbstractAuthenticationFilter.successfulAuthentication:
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request,response,authResult);
SecurityContextHolder
- SecurityContextPersistenceFilter
请求进来的时候,检查session里是否有SecurityContext,如果有则从session中取出,封装为SecurityContextHolder,放到线程环境中,没有则不处理;请求返回响应的时候,检查线程空间中是否有SecurityContext,有则取出放入session - SecurityContextHolder
封装ThreadLocal,是一个线程级的全局变量 - SecurityContext
封装Authentication - Authentication
6. 获取用户认证信息
- 在UserController中添加服务接口
@RestController
//@RequestMapping("/user")
public class UserController {
@GetMapping("/user/me")
public Object getCurrentUser() {
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/user/meAuthentication")
public Object getCurrentAuthentication(Authentication authentication) {
return authentication;
}
@GetMapping("/user/meUserDetails")
public Object getCurrentUserDetails(UserDetails userDetails) {
return userDetails;
}
.
.
.
}
- 登录用户名密码:http://localhost/index.html
- 访问:http://localhost/user/me