工具:idea2018,springboot 2.1.4,springsecurity 5.1.5html
SpringSecurity是Spring下的一个安全框架,与shiro 相似,通常用于用户认证(Authentication)和用户受权(Authorization)两个部分,常与与SpringBoot相整合。前端
便于理解,下一节再使用先后端分离,并引入数据库用户和角色信息java
(pom.xml)web
<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>
复制代码
(controller.UserController)算法
@Controller
public class UserController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello controller";
}
}
复制代码
启动项目,浏览器访问:localhost:8080/hello,地址栏自动跳转到http://localhost:8080/login,进入默认登录页面,验证登陆spring
Username默认为user
,Password随机生成(实际就是UUID),查看控制台。数据库
Spring Security默认进行URL访问进行拦截,并提供了验证的登陆页面json
输入密码,我这里目前是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);
}
}
复制代码
(为便于解释,不引入数据库信息验证)
实现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"));
}
}
复制代码
继承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接口,详情见切入源码
启动项目,清除浏览器缓存,访问hello,跳转到默认登陆页面,校验密码。登陆时,用户名任意,密码必须为123(MyUserDetailsSerice中已配置)。
登陆失败,控制台打印,没有针对id“null”PasswordEncoder(映射的密码编码器)
继承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());
}
}
复制代码
重启项目,清除浏览器缓存,访问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;
...
}
复制代码
继承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();
}
...
}
复制代码
完善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"));
}
}
复制代码
注释掉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提供的默认登录界面
(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>
复制代码
(template.index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MAIN首页</title>
</head>
<body>
<h1>欢迎来到首页</h1>
</body>
</html>
复制代码
@Controller
public class UserController {
// 登陆测试
...
// 登陆页,跳转到/templates/login.html页面
@GetMapping("/login")
public String login() {
return "login";
}
// 首页,跳转到/templates/index.html页面
@GetMapping("/index")
public String index() {
return "index";
}
}
复制代码
修改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));
}
}
复制代码
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