Spring Security学习第一天
为什么学习springSecurity?
springSecrity是解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问 控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
常用的安全框架有哪些?
Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访 问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IoC , DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入) 和 AOP(面向切面编程) 功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安 全控制编写大量重复代码的工作。
Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
为什么选择SpringSecurity?
SpringSecurity是spring全家桶的一员,SpringSecurity相比较shiro可以完成更多的需求
SpringSecurity两大概念"认证"和"授权"
认证:可以理解为登录,验证用户身份合法
授权:认证后不同用户拥有不同权限,资源是否访问由是否拥有权限决定
如何实现授权?
通常用RBAC实现,有RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,还有RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,通常采用第二种,因为前者角色变换时需要改变相应权限,后者不需要
SpringSecurity快速入门
引入相应SpringSecurity,springboot相关依赖,启动启动类即可,无需编写任何代码。即可进入SpringSecurity自带的认证页面,因为SpringSecurity默认拦截所有请求。此时观察控制有由SpringSecurity打印的一串密码,默认用户名是user。此时便可完成登录。
UserDetailsService详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,实现UserDetailsService 接口即可。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
返回类型UserDetails又是一个接口,结构如下
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取所有权限
String getPassword();//获取密码
String getUsername();//获取用户名
boolean isAccountNonExpired();//账户是否过期
boolean isAccountNonLocked();//账号是否被锁定
boolean isCredentialsNonExpired();//凭证是否过期
boolean isEnabled();//是否可用
}
所以我们选择使用Spring Security提供的实现类User返回,因为要实例化属性返回必须类接受
创建实现类实现UserDetailsService接口之前学习下PasswordEncoder 密码解析器
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容 器注入 PaswordEncoder 的bean对象。因此选择创建配置类,创建相应bean对象
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder pw(){
return new BCryptPasswordEncoder();//官方推荐密码解析器实现类
}
}
PasswordEncoder接口接口结构如下:
public interface PasswordEncoder {
String encode(CharSequence var1);//单向加密
boolean matches(CharSequence var1, String var2);
//比较,第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}//二次加密,基本不使用
}
现在开始创建实现类实现UserDetailsService接口通过自定义逻辑控制认证逻辑,(以后再使用数据库)
@Service//问题是怎么检验密码呢,这里只能传入参数username
public class LoginServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据username查询数据库
if(!"admin".equals(username)){
throw new UsernameNotFoundException("用户名或密码错误");
}
//2.根据查询的对象比较密码
String password = pw.encode("123456");
//3.返回用户对象
return new User("admin",password, AuthorityUtils.
commaSeparatedStringToAuthorityList("admin,normal"));
}
}
User类提供了两个构造方法
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
我们选择第一个,username是用户名,password是密码,authorities是用户的权限,不允许为空。此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password 和客户端传递过来的 password 进行比较。如果相同则表示认证 通过,如果不相同表示认证失败。
authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限, 如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。
通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象 的。参数是一个字符串,多个权限使用逗号分隔。
自定义登录页面
实际项目中都是使用自定义登录页面,修改配置类便可完成。
修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure 方法。
successForwardUrl() :登录成功后跳转地址
loginPage() :登录页面
loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
antMatchers() :匹配内容
permitAll() :允许
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder pw(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//表单登录
.loginPage("/login.html")//自定义登录页面
.loginProcessingUrl("/logindemo")//自定义登录逻辑
.failureForwardUrl("/toError")
.successForwardUrl("/toMain");
//授权
http.authorizeRequests()
//放行登录页面
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//所有请求都必须被认证
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
}
因为success/FalilureForwardUrl底层使用请求转发,所以只能使用post方法,所以在这里再实现controller层
@Controller
public class LoginController {
@PostMapping("/toMain")
public String login(){
return "redirect:main.html";
}
@PostMapping("/toError")
public String error(){
return "redirect:error.html";
}
}
但是这不利于我们前后端分离,由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。
所以我们可以进行自定义认证控制器,同样在配置类修改使用即可
比如:.successHandler(new MyAuthenticationSuccessHandler(“http://baidu.com”))
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public final String url;
public MyAuthenticationSuccessHandler(String url){
this.url=url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse
, Authentication authentication) throws IOException, ServletException {
/*System.out.println(authentication.getAuthorities());//权限
System.out.println(authentication.getCredentials());//密码,保护为null,凭证
System.out.println(authentication.getDetails());//返回详情
System.out.println(authentication.getPrincipal());//返回对象
System.out.println(authentication.getName());*/
httpServletResponse.sendRedirect(url);
}
}
再给出比较重要的login.html代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/logindemo" method="post">
用户名:<input type="text" name="username" /><br/>
密码:<input type="password" name="password" /><br/>
<input type="submit" value="登录" />
</form>
</body>
</html>
这里action提交地址应该与SecurityConfig中http.loginProcessingUrl()中地址相同,这里默认限制了用户名和密码必须为username,password
当进行登录之前会执行 UsernamePasswordAuthenticationFilter 过滤器。里面默认了用户名为username,密码为password。我们可以在配置类中自定义其他用户名和密码,比如
usernamePasrameter :账户参数名
passwordParameter :密码参数名
postOnly=true :默认情况下只允许POST请求。
http.formLogin()
.passwordParameter("password111")
.usernameParameter("username123");