彻底弄清SpringSecurity登录原理及开发步骤
SpringBoot+Vue之SpringSecurity登录与授权(一)
工具:idea2018,springboot 2.1.4,springsecurity 5.1.5
简介
SpringSecurity是Spring下的一个安全框架,与shiro 类似,一般用于用户认证(Authentication)和用户授权(Authorization)两个部分,常与与SpringBoot相整合。
开发步骤
便于理解,下一节再使用前后端分离,并引入数据库用户和角色信息
测试登录
1 导入依赖
(pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
复制代码
2 编写测试方法
(controller.UserController)
@Controller
public class UserController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello controller";
}
}
复制代码
3 测试
启动项目,浏览器访问:localhost:8080/hello,地址栏自动跳转到http://localhost:8080/login,进入默认登陆页面,验证登录
Username默认为user
,Password随机生成(实际就是UUID),查看控制台。
Spring Security默认进行URL访问进行拦截,并提供了验证的登录页面
输入密码,我这里目前是c1068cdb-18f3-48f4-b838-7698218d14c4
。登录成功
这里的用户名和密可以修改,直接在配置文件中修改登录名和密码,如
(application.properties)
spring.security.user.name=admin
spring.security.user.password=123
复制代码
切入源码
1> 用户参数
参照源码,查看静态内部类。可以看出,默认用户的密码实际就是一个UUID。
(SpringSecurity -- SecurityProperties.java)
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
...
// 默认用户
private User user = new User();
...
public static class User {
// 默认用户名
private String name = "user";
// 默认用户名的默认密码,随机生成
private String password = UUID.randomUUID().toString();
// 默认用户名的角色
private List<String> roles = new ArrayList<>();
// 是否生成密码
private boolean passwordGenerated = true;
...
}
}
复制代码
2> 用户名密码验证
-
导入security依赖后,默认访问的路径将经过该过滤器,并访问其无参构造,创建一个新的
post
方式的登录请求,路径为/login
。 -
进入默认登录页
-
通过HttpServletRequest对象获取到登录表单中的用户名和密码
-
创建一个用户名和密码的令牌对象
-
处理登陆表单的信息
(SpringSecurity -- UsernamePasswordAuthenticationFilter.java)
// @since spring security 3.0
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// 构造器,以不区分大小写的方式post方式和HTTP方法创建匹配器。
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 从请求路径获取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 空值判断
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
// 去除用户名首尾空格
username = username.trim();
// 生成一个用户名密码身份验证的令牌
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 设置身份认证请求的信息
setDetails(request, authRequest);
// 返回一个完全经过身份验证的对象,包括凭据
return this.getAuthenticationManager().authenticate(authRequest);
}
....
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
}
复制代码
自定义登录接口
(为便于解释,不引入数据库信息验证)
1 实现接口
实现UserDetailsService
接口,重写方法。
(service.MyUserDetailsSerice)
/**
* 自定义登录接口(核心接口,加载用户特定的数据。)
*/
@Component
public class MyUserDetailsSerice implements UserDetailsService {
// 日志 返回与作为参数传递的类对应的日志程序
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
/**
* 校验,根据用户名定位用户
* @param username 标识需要其数据的用户的用户名。
* @return 核心用户信息,一个完全填充的用户记录
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录,用户名:{}", username);
return new User(username, "123", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
复制代码
2 配置登录拦截
继承WebSecurityConfigurerAdapter
配置类,重写里面的配置方法
配置方法可查看官网springboot或查看EnableWebSecurity
接口的注释信息
(config.MySecurityConfig)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
// 基础配置
http.httpBasic()
.and()
// 身份认证
.authorizeRequests()
// 所有请求
.anyRequest()
// 身份认证
.authenticated();
}
复制代码
返回的User实现了UserDetail接口,详情见切入源码
3 测试
启动项目,清除浏览器缓存,访问hello,跳转到默认登录页面,校验密码。登录时,用户名任意,密码必须为123(MyUserDetailsSerice中已配置)。
登录失败,控制台打印,没有针对id“null”PasswordEncoder(映射的密码编码器)
4 加入密码编码器组件
继承PassawordEncoder接口
/**
* 用于编码密码的服务接口的实现类。
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 编码原始密码。通常,良好的编码算法应用SHA-1或更大的哈希与8字节或更大的随机生成的盐相结合。
* @param rawPassword 密码,一个可读的字符值序列
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/**
* 验证从存储中获得的编码密码是否与提交的原始密码匹配。如果密码匹配,返回true;如果不匹配,返回false。存储的密码本身永远不会被解码。
* @param rawPassword 预设的验证密码。要编码和匹配的原始密码
* @param encodedPassword 表单输入的密码。来自存储的编码密码与之比较
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(rawPassword.toString());
}
}
复制代码
4 测试
重启项目,清除浏览器缓存,访问hello。
切入源码
1 关于WebSecurityConfigurerAdapter
可参考接口EnableWebSecurity
(SpringSecurity -- EnableWebSecurity)
/**
* Add this annotation to an {@code @Configuration} class to have the Spring Security
* .............
* @Override
* protected void configure(HttpSecurity http) throws Exception {
* http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest()
* .hasRole("USER").and()
* // 更多配置 ...
* .formLogin() // 确保基础表单登录
* // 为所有与表单登录相关联的URL设置许可证
* .permitAll();
* }
*
* ...................
* @since 3.2
*/
...
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
// 默认关闭debug模式
boolean debug() default false;
}
复制代码
2 security中封装的默认用户User的信息 (SpringSecurity -- User.java)
//
public class User implements UserDetails, CredentialsContainer{
...
private String password;
private final String username;
// 用户权限集合
private final Set<GrantedAuthority> authorities;
// 账户未过期
private final boolean accountNonExpired;
// 账户未锁定
private final boolean accountNonLocked;
// 凭据未过期
private final boolean credentialsNonExpired;
// 用户可用
private final boolean enabled;
...
}
复制代码
密码加密
1 注入密码编码器对象
继承WebSecurityConfigurerAdapter
配置类
在MySecurity中直接注入一个BCryptPasswordEncoder
对象。它实现了PasswordEncoder
接口,并重写了encode
和matches
方法
(config.MySecurityConfig.java)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 实现使用BCrypt强哈希函数的密码编码器。客户机可以选择性地提供“强度”(即BCrypt中的日志轮数)和SecureRandom 实例。
* 强度参数越大,需要做的工作就越多(指数级)来散列密码。默认值是10。
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
复制代码
2 完善服务层
完善MyUserDetailsSerice
(service.MyUserDetailsSerice.java)
@Component
public class MyUserDetailsSerice implements UserDetailsService {
...
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123");
logger.info("登录,用户名:{},密码:{}", username,password);
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
复制代码
3 测试
注释掉MyPasswordEncoder
的@component注解,使其失去容器组件身份
使用debug模式,启动项目,访问hello。
debug可看到密码的转化,原始密码123加密为为$2aYGYb9i0ZjnTHPlOk/NQb/efrPNOaJq8hJYtdXf8VcdQUi8T8S3Iim
控制台打印日志
切入源码
可以看到,这里自动注入的其实是BCryptPasswordEncoder
对象,并调用了encode方法
(SpringSecurity -- BCryptPasswordEncoder)
// 构造器
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
...
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
...
}
...
public String encode(CharSequence rawPassword) {
// 盐值
String salt;
// 判断构造器是否有相应参数
if (strength > 0) {
if (random != null) {
// 通过random和strength生成的salt
salt = BCrypt.gensalt(strength, random);
}
else {
// 通过strength生成的salt
salt = BCrypt.gensalt(strength);
}
}
// 无参构造
else {
// 调用gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);随机生成salt
// GENSALT_DEFAULT_LOG2_ROUNDS = 10
salt = BCrypt.gensalt();
}
// 使用OpenBSD bcrypt方案散列密码,参数分别为原始密码和盐值
return BCrypt.hashpw(rawPassword.toString(), salt);
}
复制代码
-
这里BCryptPasswordEncoder使用的无参,使用默认的盐值,循环10次,生成了散列的密码。
-
这里虽然是123,但每次加密后都不相同,Spring Security在进行密码加密的时候,生成了一份随机salt,最终加密的密码=密码+随机salt。
-
注意这里的AuthorityUtils的方法,参数包含角色信息。实际业务中,一般以“ROLE_**”规定用户的角色字段,并在登录后授予相应权限
/**
*从逗号分隔的字符串表示创建一个GrantedAuthority对象数组(例如“ROLE_A,ROLE_B,ROLE_C”)
*@param authorityString 逗号分隔的字符串
*@return 通过标记字符串创建的权限
/
AuthorityUtils.commaSeparatedStringToAuthorityList("admin")
复制代码
自定义登录请求
不使用springsecurity提供的默认登陆界面
1 自定义前端登录页
(template.login.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>欢迎登录</h2>
<form action="/auth/login" method="post">
<input name="username" type="text" placeholder="请输入用户名.."><br/>
<input name="password" type="password" placeholder="请输入密码.."><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
复制代码
2 自定义首页
(template.index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MAIN首页</title>
</head>
<body>
<h1>欢迎来到首页</h1>
</body>
</html>
复制代码
3 在控制器类中添加跳转路径
@Controller
public class UserController {
// 登录测试
...
// 登录页,跳转到/templates/login.html页面
@GetMapping("/login")
public String login() {
return "login";
}
// 首页,跳转到/templates/index.html页面
@GetMapping("/index")
public String index() {
return "index";
}
}
复制代码
4 修改拦截配置
修改MySecurityConfig中configure方法
(config.MySecurityConfig.java)
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
// 表单认证
.formLogin()
// 登录页
.loginPage("/login")
// 登录表单提交地址
.loginProcessingUrl("/auth/login")
.and()
// 身份认证请求
.authorizeRequests()
// URL路径匹配
.antMatchers("/login").permitAll()
// 任意请求
.anyRequest()
// 身份认证
.authenticated();
}
}
复制代码
loginProcessingUrl("/auth/login")
中定义了表单提交地址,但在控制器UserController中并没有对应的请求路径,SpringSecutity默认拦截所有请求,并将URL 302重定向到/login默认登录页,使用默认的用户名密码即可登录。
自定义登录请求状态
方式一:继承接口实现
1 自定义登录成功类
(handler.MyAuthenticationSuccessHandler.java)
/**
* 继承接口,用于处理成功的用户身份验证的策略
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
// 提供了读取和写入JSON的功能,可以与基本pojo类进行交互,也可以与通用JSON树模型进行交互,还提供了执行转换的相关功能。
@Autowired
private ObjectMapper objectMapper;
// 当用户已成功通过身份验证时调用。
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
response.setContentType("application/json;charset=utf-8");
// writeValueAsString:将java对象序列化为字符串
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
复制代码
2 自定义登录失败类
(handler.MyAuthenticationFailureHandler.java)
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败");
// http状态,200,成功
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}
}
复制代码
方式二:修改MySecurityConfig的配置方法
1 添加登录成功和失败的处理方法
(config.MySecurityConfig.java)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/auth/login")
// 登陆成功处理器
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
ObjectMapper om = new ObjectMapper();
String successMsg = om.writeValueAsString(om.writeValueAsString(authentication));
writer.write(successMsg);
writer.flush();
writer.close();
}
})
// 登陆失败处理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString(e));
writer.flush();
writer.close();
}
})
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated();
}
复制代码
获取当前用户信息
(controller.UserController.java)
@Controller
public class UserController {
...
// 当前用户信息
@GetMapping("/info")
@ResponseBody
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}
复制代码
测试
启动项目,访问/info,登录成功,检查F12
来源:https://juejin.cn/post/6844903834402193415