1. SpringSecurity介绍
官网链接: https://spring.io/projects/spring-security#learn
2. 搭建Demo工程
选择从Spring Initializr
创建SpringBoot项目,勾选依赖如下:
2.1 简单的页面测试
这里写了一个简单登陆页面和一个简单的登陆接口
@RequestMapping("/login")
public String login(){
return "redirect:main.html";
}
<form action="/login" method="post">
用户名:<input type="text" name="username123"/><br/>
密码:<input type="password" name="password123"/><br/>
<input type="submit" value="登陆"/><br/>
</form>
当我们启动应用,并且访问登陆页面时(localhost:8080/login.html
),出现如下页面:
并且在IDEA的输出控制中可以看到:
这是Spring Security的默认登陆逻辑。在我们没有配置自己的登陆逻辑情况下,所有访问服务器的请求都会被拦截,出现上面的登陆界面。默认的登陆名是user,密码就是IDEA输出控制台中那串临时密码,且每次启动应用后,生成的密码都不一样。
输入了正确的用户名和密码之后,就跳转到了我们自定义的登陆页面:
2.2 自定义登陆逻辑
自定义登陆逻辑需要实现UserDetailsService
接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.从数据库查询username,查不到则报异常(这里就不连数据库了,为了测试,直接写死)
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
// 2.比较密码
// 一般情况下是需要查询数据库,将数据库的密码(加密保存的)和传进来的密码进行比较
String encodePwd = passwordEncoder.encode("123");
// AuthorityUtils是一个工具类,这里直接指定该user的权限列表,并且拥有的角色是abs(ROLE_ 是固定写法)
// 入参的GrantedAuthority接口我们也可以自己实现。然后这里直接new我们的实现类即可
return new User(username, encodePwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc,/main.html"));
}
}
在一般的开发中,我们需要对传入的
username
进行数据库查找,找到之后才进行密码比对。这里为了测试,直接将用户名和密码都写死了。用户名是admin,密码是123需要注意的是,在实际开发中,无论我们保存进数据库的密码或者是前端传给后端的密码,都是加密后的密文。我们在比较密码时,前端传过来的密码进行解密,后端从数据库中查询出的密码进行解密,两者都是解密后的密码,然后进行比较。
这里返回的User对象是SpringSecurity中定义的,其中包含了一些登陆用户的基本信息(用户名、密码、权限列表、账户状态等),注意不要与自定义的User实体类混淆。
2.3 PasswordEncoder
这里的PasswordEncoder
是用来对密码进行加密和解密的,我们在SecurityConfig
(后面会讲到)中自定义其实现类为BCryptPasswordEncoder
,这是官方推荐的一种加密方式。同时,我们将PasswordEncoder
交给了Spring
管理,使得我们需要时,可以直接使用@Autowired
注入bean
来使用。
// 将PasswordEncoder交给spring容器进行管理
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
编写完成自定义的登陆逻辑后,我们还要配置Spring Security,不然这个登陆逻辑不会生效。
2.4 SpringSecurity配置
在demo中新建一个配置类,需要继承自WebSecurityConfigurerAdapter
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository tokenRepository;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 自定义入参,参数名要和前端页面提交的参数名一样,不然会报错
.usernameParameter("username123").passwordParameter("password123")
// 自定义登陆页面
.loginPage("/login.html")
// 必须和表单提交的接口一样,去执行自定义的登陆逻辑
.loginProcessingUrl("/login")
// 登陆成功后跳转的页面 POST请求
.successForwardUrl("/toMain")
// 自定义登陆成功跳转逻辑
//.successHandler(new MyAuthenticationSuccessHandler("/main.html"))
// 登陆失败后跳转的页面 POST请求
// .failureForwardUrl("/toError");
// 自定义登陆失败跳转逻辑
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
// 自定义异常处理
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
// 关闭csrf防护
http.csrf().disable();
// 授权
http.authorizeRequests()
// 放行login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 放行error.html ,不需要认证
.antMatchers("/error.html").permitAll()
// images目录下的所有资源放行
// .antMatchers("/images/**").permitAll()
// 所有目录下的jpg后缀的文件都放行
.antMatchers("/**/*.jpg").permitAll()
// 权限控制,拥有admin权限才可访问(严格区分大小写)
//.antMatchers("/main1.html").hasAuthority("admin")
// 权限控制,拥有[admin,admiN]其中一个权限即可访问
//.antMatchers("/main1.html").hasAnyAuthority("admin","admiN")
// 基于角色的权限控制(严格区分大小写)
//.antMatchers("/main1.html").hasAnyRole("abc")
// 基于access的访问控制
//.antMatchers("/main1.html").access("hasRole('abc')")
// 基于角色的权限控制,有任意一个角色即可访问
//.antMatchers("/main1.html").hasAnyRole("abc", "abC")
// 所有请求必须认证才能访问,也就是需要登陆(这句代码必须放在最后)
.anyRequest().authenticated();
// 自定义access方法
//.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
// 设置 记住我
http.rememberMe()
// 设置数据源
.tokenRepository(tokenRepository)
// 设置过期时间(秒)
.tokenValiditySeconds(30)
// 设置自定义登陆逻辑
.userDetailsService(userDetailsService);
// 退出
http.logout()
// 退出url,默认就是springsecurity提供的/logout
.logoutUrl("/logout")
// 退出成功后跳转的url
.logoutSuccessUrl("/login.html");
}
// 自定义token存储方法
@Bean
public PersistentTokenRepository tokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository=new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 设置自定建表(数据库自定建表存储登陆信息,第一次启动时开启,第二次要注释掉)
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
// 将PasswordEncoder交给spring容器进行管理
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.5 自定义登陆成功或者失败的处理
自定义登陆成功的Handler
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private String url; public MyAuthenticationSuccessHandler(String url) { this.url = url; } // 自定义登陆成功跳转逻辑 @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { //// 获取user对象,就是我们在自定义登陆逻辑那里返回的user //User user = (User) authentication.getPrincipal(); //System.out.println(user.getUsername()); //System.out.println(user.getPassword()); //System.out.println(user.getAuthorities()); // 使用重定向跳转页面 httpServletResponse.sendRedirect(url); } }
自定义登陆失败的Handler
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { private String url; public MyAuthenticationFailureHandler(String url) { this.url = url; } // 自定义登陆失败跳转逻辑 @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.sendRedirect(url); } }
自定义异常处理Handler
// 自定义权限异常处理 @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { // 设置状态码 httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); // 返回json格式 httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}"); writer.flush(); writer.close(); } }
2.6 自定义Access方法
我们在SecurityConfig中的授权配置http.authorizeRequests(),本质上都是在执行一个access方法,因此我们可以自定义这个方法。
/**
* 这个是自定义的access逻辑
*/
@Service
public class MyServiceImpl implements MyService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 得到主体(这里的主体就是我们在自定义登陆逻辑时返回的user)
Object obj = authentication.getPrincipal();
// 如果主体的类型是UserDetails,那就转成UserDetails
if (obj instanceof UserDetails) {
UserDetails userDetails = (UserDetails) obj;
//获取权限列表
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 如果权限列表中包含了请求uri,则返回true
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
2.7 RememberMe
平时在进行登陆时,都会有一个记住我
可以勾选。勾选之后,在一定时间内,用户可以不必重复登陆,就可以访问资源。
这个设置在上面SecurityConfig
也有。这里需要注意的是,我们可以自定义token
存储方式。
当我们启用RememberMe功能的时候,第一次登陆时,系统会生成一个token,存储在本地或者内存中。下次登陆时,系统会去查找这个token,如果没有过期,则不用登陆,可以直接访问。如果过期了则需要重新登陆。
token存储方式有两种,一种是存储在数据库中,一种存储在内存中,这里我们自定义token存储在数据库中。
// 自定义token存储方法
@Bean
public PersistentTokenRepository tokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository=new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 设置自定建表(数据库自定建表存储登陆信息,第一次启动时开启,第二次要注释掉)
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
需要注意的是代码中的设置自动建表。同时,我们需要在项目中配置数据源,并且在数据库中建立对应的数据库
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: admin123
2.8 自定义退出逻辑
SecurityConfig
代码中已经写有。
需要注意的是,SpringSecurity提供了一个内置的logout接口,我们一般直接用这个接口就好,不用修改。注意前端页面也要访问这个接口.
2.9 基于注解的访问控制
这里只讲两个注解,更多的注解使用请查看官网
要使用SpringSecurity的注解,需要在启动类上开启,并且需要具体指定该注解的Enabled = true
@SpringBootApplication
// 开启SpringSecurity的注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class DemoSpringsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSpringsecurityApplication.class, args);
}
}
@Secured
// Spring Security的注解:拥有abc角色的用户可以访问此接口
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String main() {
return "redirect:main.html";
}
@PreAuthorize
// 表示在访问前控制,判断用户是否具有角色'abc',有则允许访问该接口
// PreAuthorize在这个注解中hasRole的值可以用ROLE前缀,也可以不用。但是配置类中不用加前缀
@PreAuthorize("hasRole('abc')")
@RequestMapping("/toMain")
public String main() {
return "redirect:main.html";
}