本文没有任何原理解释,没有任何使用讲解,而且记录的是比较基本的操作,不适合初学者入门,适合已学过后的方法笔记以便于下次快速上手使用~
如有错误,请指出,感谢观看!
一.环境介绍
本文采用环境为SpringBoot2.1.3,并且使用了Thymeleaf模板引擎(暂未使用前后端分离,以后会写),目前暂时不使用数据库连接,所有需要数据库模拟操作的地方,我会指出。
目录介绍和需求介绍:
测试环境暂时只是一个TestController,所有请页面均写在此处,TestSecurityConfig为SpringSecurity的配置类。
目前暂时使用三个页面,test1和test2页面分别代替真实环境中不同的业务页面(比如:学生管理,作业管理等),需要我们权限控制访问。
访问成功后会有如下提示:
二.使用SpringSecurity
下面让我们用简单的模拟来熟悉SpringSecurity的基本使用吧!
1.引入SpringSecurity
我们想要使用SpringSecurity,只需要在pom文件中添加入如下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加后,重启项目,就可以看到SpringSecurity已经生效(以前不需要登陆的页面需要登陆才可使用):
登陆的用户名为user,密码会在我们控制台输出:
输入即可登录系统!
2.HttpBasic登录模式
如果我们不需要单独的登录页面,安全性的需求不是很高,我们可以让SpringSecurity使用最基本的HttpBasic登录模式!
使用此模式,我们需要在TestSecurityConfig配置类中重写WebSecurityConfigurerAdapter的配置,代码如下:
@Configuration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf)跨站请求伪造攻击防御(开启后可能会拦截测试请求,导致登录失败)
.httpBasic()
.and()
.authorizeRequests()//定义哪一些请求需要被权限认证保护
.anyRequest()//所有请求都会被保护
.authenticated();//登陆后才可以访问
}
}
配置后,我们直接使用简单的登陆弹框,验证后即可访问页面!
3.FormLogin登录模式与自定义登录页
比起HttpBasic登录模式,FormLogin更适合我们的业务需求,SpringSecurity默认为FormLogin登录模式!
Formlogin可以设置自定义登录页,代码如下:
@Configuration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf)跨站请求伪造攻击防御(开启后可能会拦截测试请求,导致登录失败)
.formLogin()//formLogin登录模式
.loginPage("/login")//设置登录页,未登录访问会自动跳转到该登录页
.usernameParameter("username")//设置登录页提交的用户名的字段名称
.passwordParameter("password")//设置登录页提交密码的字段名称
.defaultSuccessUrl("/index")//默认登陆成功之后跳转的页面
.failureForwardUrl("/login")//失败后跳转的页面
.and()
.authorizeRequests()//定义哪一些请求需要被权限认证保护
.antMatchers("/login")//登录页设置访问权限
.permitAll()//允许所有用户访问
.anyRequest()//所有请求都会被保护
.authenticated();//登陆后才可以访问
}
}
注意:
- 和以往不同的是,SpringSecurity的登陆不需要自己手写登陆的逻辑,只需要编写登录页即可,SpringSecurity通过自己的拦截器,拦截发送的/login的post请求,所以我们只需要向/login发送登陆请求即可!
- 不管是通过表单,还是通过ajax,都可以向/login发送请求进行登录,但需要注意后端绑定的字段需要与前端发送的字段相同,否则SpringSecurity无法正确获取登录信息。
在此处测试我们就可以发现,未登录跳转到登录页,成功失败后均跳转到相应页面!
注意:
.defaultSuccessUrl("/index")//默认登陆成功之后跳转的页面
我们可以使defaultSuccessUrl方法设置登陆成功后跳转的url,但我们测试会发现,如果我们直接访问其它页面被拦截(比如http://localhost:8080/test2)的话,登录成功后会直接跳转到之前访问的页面(直接跳转到/test2对应的页面),如果我们需要登录成功后跳转到指定页面,则可以适用如下方法:
.successForwardUrl("/index")//登陆成功后访问的页面(无论之前访问什么页面,都跳转到此页面)
注意:
如果以上测试不生效,则有可能是权限控制代码顺序的问题:
注意代码顺序一定不能颠倒,上方设置完成后下方再设置就会不生效,如果把绿框代码放到上方,就会导致登录页也会被拦截,所以编写此代码时,一定要把大范围的授权放在下方,而具体授权规则的代码放在上方!
4.从数据库获取用户信息(模拟)
我们实际的使用中都是通过RBAC权限将用户的所有信息查询出来,但本文章并不会实际查询,只是模拟查询,如果文章由看不懂的地方,可以去完整学习SpringSecurity,本文只是学习后的记录,以便于下次快速使用,并不是完整教程,抱歉~
4.1.密码加密操作和BCryptPasswordEncoder类
我们的数据库存储的密码一定不能是明文,为了避免出现安全问题,我们可以使用BCryptPasswordEncoder类将密码加密后存储到数据库中。
示例:将字符串“admin”加密
String password = new BCryptPasswordEncoder().encode("admin");
System.err.println(password);
结果
我们可以直接在添加用户时,将用户设置的密码直接加密为此格式,使用数据库存储即可!
4.2.自定义用户登录逻辑
我们想要自定义登录逻辑,首先需要实现UserDetailsService接口,接口内规定了实现自定义登录逻辑的方法!
之后在配置类中将此接口设置为自定义登录逻辑(注意:因为之前使用了密码加密,所以也要讲密码加密方式一起设置,否则框架无法判断加密后的密码是否正确):
@Autowired
private MyUserDetailService myUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
此时我们就可以使用自定义密码输入逻辑,再接口方法中,我们会接受到前段输入的用户名信息,我们可以在此方法中查询数据库,然后将查询出的信息写入UserDetails类中返回即可!(注意:因为之前配置了密码加密类,所以我们并不需要在查询出密码后手动解密,只需要将数据库中查询出来的密码的密文返回即可,框架会自动解密并判断前端输入的密码是否正确):
@Component
@Slf4j
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//先在这里模拟数据库操作,假设这些是数据库出来的
MyUserDetails userDetails = new MyUserDetails();
userDetails.setUsername(userName);//登录的用户名
//此处应将从数据库查询出来的加密后的密码取出(因为没有数据库存,所以直接模拟一下)
String password = new BCryptPasswordEncoder().encode("admin");
userDetails.setPassword(password);//登陆的密码
userDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList("menu1"));//用户具有的权限
return userDetails;//将UserDetails对象返回,框架会自定分析密码是否正确,是否具有权限,无需手动操作
}
}
其中:UserDetails为接口,里面规定了框架获取用户信息的方式,简单实现即可
public class MyUserDetails implements UserDetails {
private String username;//用户名
private String password;//密码
private boolean accountNonExpired;//是否没过期
private boolean accountNonLocked;//是否锁定
private boolean credentialsNonExpired;//是否没过期
private boolean enabled;//是否可用
private Collection<? extends GrantedAuthority> authorities;//权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
/*自己定义的构造方法用于赋值*/
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
SpringSecurity框架主要分为认证和授权两大操作,认证即登录,目前认证逻辑全部完成!
5.资源权限设置与用户的授权
在SpringSecurity中,我么可以为每一个url,方法等设置访问权限,用户登录时,通过数据库查询出用户具有哪些权限,如果没有相应权限,禁止访问,有权限则放行访问!
5.1.UserDetails用户授权
在我们之前实现过的UserDetails类中,有一个权限即可,我们要为用户添加权限,直接将从数据库查询出来的权限即可添加即可!
userDetails.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList("menu1,menu2,test1,test2"));
假设我们从数据库中查询到当前登录用户有如上四个权限,只需将其写成逗号间隔的形式,设置即可!
5.2.为资源设置访问权限——url方式设置
在我们之前的配置中,这样的方式就是为url设置权限(上方代码就是为“/login”url设置了任意用户均可访问的权限)
结合之前的测试页面,我们为“test1”设置“test1:check”权限,为“test2”设置“test2:check”权限!
@Configuration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf)跨站请求伪造攻击防御(开启后可能会拦截测试请求,导致登录失败)
.formLogin()//formLogin登录模式
.loginPage("/login")//设置登录页,未登录访问会自动跳转到该登录页
.usernameParameter("username")//设置登录页提交的用户名的字段名称
.passwordParameter("password")//设置登录页提交密码的字段名称
.defaultSuccessUrl("/index")//默认登陆成功之后跳转的页面
.failureForwardUrl("/login")//失败后跳转的页面
.and()
.authorizeRequests()//定义哪一些请求需要被权限认证保护
.antMatchers("/login")//登录页设置访问权限
.permitAll()//允许所有用户访问
.antMatchers("/test1")
.hasAnyAuthority("test1:check")
.antMatchers("/test2")
.hasAnyAuthority("test2:check")
.anyRequest()//所有请求都会被保护
.authenticated();//登陆后才可以访问
}
}
这样,如果我们为用户只设置了test1:check权限,该用户就可以访问/test1页面,但访问/test2权限时就会发生异常,因为该用户并没有访问此url的权限!
5.3.为资源设置访问权限——方法设置权限
除了为URL设置权限外,我们还可以为方法设置权限!
首先,我们需要在配置类上加入以下注解:
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启方法级别的权限验证
开启方法级别的权限认证后,我们就可以通过注解为方法加入权限:
@PreAuthorize("权限认证符")
例如:为“test1”设置“test1:check”权限,为“test2”设置“test2:check”权限
@GetMapping("/test1")
@PreAuthorize("hasAnyAuthority('test1')")
public String test1()
{
return "test1";
}
@GetMapping("/test2")
@PreAuthorize("hasAnyAuthority('test2')")
public String test2()
{
return "test2";
}
次方法设置权限与上方url设置权限效果基本相同,并且功能更加强大,除了在方法执行前验证,还可以在方法执行后验证返回值等,与url设置相同,同样可以为一个方法设置多个权限!
@PreAuthorize("hasAnyAuthority('test2') or hasAnyAuthority('menu2')")//只要具有其中任意权限即可访问
注意:此方式并不仅限于Controller验证,在Service,Dao中仍然可以使用,但建议控制Controller方法!
6.自定义登陆成功与失败页面
登录成功页面:
继承SavedRequestAwareAuthenticationSuccessHandler类并重写onAuthenticationSuccess方法即可!
登录失败页面:
继承SimpleUrlAuthenticationFailureHandler类并重写onAuthenticationFailure方法即可!
重写完成后需在配置类中进行如下配置:
.failureHandler(myFaildHandler)//设置自定义逻辑处理登录失败
.successHandler(mySuccessHandler)//设置自定义逻辑处理登录成功
完整配置:
@Configuration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MySuccessHandler mySuccessHandler;
@Autowired
private MyFaildHandler myFaildHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf)跨站请求伪造攻击防御(开启后可能会拦截测试请求,导致登录失败)
.formLogin()//formLogin登录模式
.loginPage("/login")//设置登录页,未登录访问会自动跳转到该登录页
.usernameParameter("username")//设置登录页提交的用户名的字段名称
.passwordParameter("password")//设置登录页提交密码的字段名称
.failureHandler(myFaildHandler)//设置自定义逻辑处理登录失败
.successHandler(mySuccessHandler)//设置自定义逻辑处理登录成功
.and()
.authorizeRequests()//定义哪一些请求需要被权限认证保护
.antMatchers("/login")//登录页设置访问权限
.permitAll()//允许所有用户访问
.antMatchers("/test1")
.hasAnyAuthority("test1:check")
.antMatchers("/test2")
.hasAnyAuthority("test2:check")
.anyRequest()//所有请求都会被保护
.authenticated();//登陆后才可以访问
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
}
登陆成功处理代码:
@Component
public class MySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
//在这里实现自定义逻辑
}
}
登录失败处理代码:
@Component
public class MyFaildHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//在这里实现自定义逻辑
}
}
其中:登陆成功逻辑处还可获取一个Authentication对象
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//权限信息列表,GrantedAuthority类,通常是代表权限信息的一系列字符串。
Object getCredentials();//密码信息,用户输入的密码字符串,在认证过后通常会被移除。
Object getDetails();//WebAuthenticationDetails类,包括用户的ip地址和sessionId的值等。
Object getPrincipal();//大部分情况下返回的是UserDetails接口的实现类
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
所以,我们就可以通过此对象获取登录用户的详细信息:
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
System.err.println(userDetails.getUsername());
System.err.println(userDetails.getPassword());
7.单点登录
在SpringSecurity中想要实现单点登录,只需要设置允许最大Session个数即可,并且将可重新登陆的选项打开,这样,当新用户登陆后,之前登录的用户会自动失效!
.and()
.sessionManagement()//管理Session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)//在必要时创建session
.sessionFixation()//重新登陆后session策略
.migrateSession()//复制Session信息到新的session
.maximumSessions(1)//同一用户允许最大Session个数
.maxSessionsPreventsLogin(false)//已登录用户是否允许重新登陆
自定义session失效逻辑
当session失效后,我们仍可以自定义session失效逻辑,只需要实现SessionInformationExpiredStrategy接口即可!
@Component
public class MySessionStrategyHandler implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().getWriter().println("session过期,请重新登陆");//可以通过event得到resopnse和resquest对象
}
}
逻辑定义完成后,在配置类中定义即可:
.expiredSessionStrategy(mySessionStrategyHandler)//自定义session失效逻辑
8.自定义退出逻辑
要想自定义退出逻辑,我们只需要在配置类中配置即可:
.and()
.logout()//logout配置
.logoutUrl("/logout")//自定义退出url
.logoutSuccessUrl("/login");//退出成功后跳转的页面
上方代码,我们将退出url设置为“/logout”,这样我们只需要访问此链接,就会自动退出登录!
退出后,当前用户对应的session信息会被清除,这时即使跳转回之前的页面,也需要重新登陆才可访问!
如果要自定义复杂退出逻辑,实现LogoutSuccessHandler实现其中方法即可!
@Component
public class MyLoguotHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//写一些自定义的业务逻辑
}
}
.and()
.logout()//logout配置
.logoutUrl("/logout")//自定义退出url
.logoutSuccessHandler(myLoguotHandler);//自定义的退出逻辑!
9.记住登陆
如果我们需要长时间免登录直接可访问功能,我们就可以使用“记住我”功能!
开启记住我功能很简单,只需要在配置类中开启即可:
.and()
.rememberMe();//开启记住我功能
然后在前端页面加入选项即可!
如果我们想控制记住我的时间,在配置类中自定义接口(默认为两周):
.and()
.rememberMe()
.tokenValiditySeconds(60 * 60 * 24 * 7);//设置记住我时间
如果要更改前端传入的字段名称,也可以修改:
.and()
.rememberMe()
.tokenValiditySeconds(60 * 60 * 24 * 7)//设置记住我时间
.rememberMeParameter("remember-me");//设置记住我的字段
无论设置为什么字段,只需要在登录时的post表单中一同提交即可,非常方便~
感谢大家查看我的文章,接下来还会有更多SpringSecurity的内容~