Spring Security 入门(基本使用)
这几天看了下b站关于 spring security 的学习视频,不得不说 spring security 有点复杂,脑袋有点懵懵的,在此整理下学习内容。
1、入门
个人理解url 的访问流程大致如下:
1.1、什么是 spring security
- spring security 是一个比 shiro 更加强大的安全管理框架,权限颗粒度更细。
- 源自于 spring 家族,能跟 springboot 无缝整合,对 oauth2 的支持也更好。
1.2、依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.3、测试接口
添加一个简单的 /hello 接口:
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "恭喜你登录成功";
}
启动项目,访问 /hello 接口,会发现自动跳转到 spring security 提供的登录页面:
默认的 username 为 :user,password 在项目启动时随机生成,具体如下:
登录成功后即可访问 /hello接口。
2、自定义登录页面、登录成功处理器、登录失败处理器、异常处理器、权限逻辑
项目结构如下:
2.1、自定义登录页面
1、登录页面 login.html :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<form method="post" action="/login">
用户名:<input type="text" name="username123"><br />
密码:<input type="password" name="password123"><br />
<button type="submit">立即登陆</button>
</form>
</body>
</html>
2、登录成功跳转页 main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!!!
<a href="/main1.html">跳转权限页</a>
</body>
</html>
3、登录失败跳转页 error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录失败,请重新登录<a href="/login.html">跳转</a>
</body>
</html>
4、权限页 main1.html
**main.html **如果有权限,则能访问该页面,否则报 403
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限控制!!!</a>
</body>
</html>
2.2、自定义登录逻辑
自定义登录逻辑主要用于对用户名和密码进行校验,需要实现 UserDetailService 接口
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("=======执行自定义登录逻辑====");
//校验用户名,实际环境中需要从数据库查询
if (!username.equals("admin")) {
throw new UsernameNotFoundException("用户不存在");
}
//比较密码,实际需要从数据库取出原密码校验,框架会自动读取登录页面的密码
String password = bCryptPasswordEncoder.encode("123456");
//返回UserDetails,实际开发中可拓展UserDetails
return new User(username, password,
//自定义权限
AuthorityUtils.commaSeparatedStringToAuthorityList("permission1"));
}
}
2.3、自定义登录成功处理器
登录成功处理器实现 AuthenticationSuccessHandler 接口
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//获取IP地址
System.out.println(request.getRemoteAddr());
//获取认证用户信息
User user = (User) authentication.getPrincipal();
System.out.println("=====" + user.getAuthorities());
//重定向
response.sendRedirect(url);
}
}
2.4、自定义登录失败处理器
登录失败处理器实现 AuthenticationFailureHandler接口
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { private String url; public MyAuthenticationFailureHandler(String url) { this.url = url; } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //重定向 response.sendRedirect(url); }}
2.5、自定义异常处理器
@Componentpublic class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { //响应状态403 response.setStatus(HttpServletResponse.SC_FORBIDDEN); //返回格式 response.setHeader("Content-Type", "application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write("{status: "error","msg": "权限不足,请联系管理员"}"); writer.flush(); writer.close(); }}
2.6、配置 Spring Security
该类是 Spring Security 的配置类, 继承 WebSecurityConfigurerAdapter
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; /** * 指定密码加密的方法 * * @return */ @Bean public BCryptPasswordEncoder getPasswordEncode() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义用户名和密码参数 .usernameParameter("username123") .passwordParameter("password123") //自定义登录页面 .loginPage("/showLogin") //必须和表单提交的接口一样,执行自定义登录逻辑 .loginProcessingUrl("/login") //自定义登录成功处理器 .successHandler(new MyAuthenticationSuccessHandler("/main.html")) //自定义登录失败处理器 .failureHandler(new MyAuthenticationFailureHandler("/error.html")); //授权 http.authorizeRequests() //放行/login.html,不需要认证 .antMatchers("/showLogin").permitAll() //放行/error.html,不需要认证 .antMatchers("/error.html").permitAll() //基于权限判断 .antMatchers("/main1.html").hasAuthority("permission1") //所有请求必须认证 .anyRequest().authenticated(); //异常处理器 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); //关闭csrf防护 http.csrf().disable(); } /** * 放行静态资源,css,js,images * * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**") .antMatchers("/**/*.png"); }}
2.7、运行测试
1、运行后访问 http://localhost:8080/login.html
,加载的自定义登录页面如下:
注意我在前面的自定义登录逻辑中写死了 username: admin和password:123456
2、点击立即登陆按钮,根据登录成功处理器重定向到登录成功页 main.html:
3、前面的代码中,如果登录成功则拥有permission1权限,而访问权限页刚好需要 permission1 权限,
点击跳转权限页,来到权限页** main1.html**:
4、修改登录成功的权限为 permission2,
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("=======执行自定义登录逻辑===="); //校验用户名,实际环境中需要从数据库查询 if (!username.equals("admin")) { throw new UsernameNotFoundException("用户不存在"); } //比较密码,实际需要从数据库取出原密码校验,框架会自动读取登录页面的密码 String password = bCryptPasswordEncoder.encode("123456"); //返回UserDetails,实际开发中可拓展UserDetails return new User(username, password, //修改权限为permisson2 AuthorityUtils.commaSeparatedStringToAuthorityList("permission2"));}
再次访问需要 permission1 权限的权限页,打印以下错误:
5、如果 username 或者 password 错误,根据登录失败处理器重定向到登录失败页 error.html:
3、自定义用户退出登录
3.1、默认的退出登录
spring security 有默认的退出登录接口,直接访问 /logout 接口,就能实现退出登录,下面是简单演示:
main.html 添加退出登录的访问链接logout
:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title></head><body>登录成功!!!<a href="/logout">退出</a><a href="/main1.html">跳转权限页</a></body></html>
直接就能退出了,简不简单呢?默认跳转到登录页:
仔细观察,发现访问路径拼接了 ?logout 字符串,查看源码可以发现默认的配置如下:
3.2、自定义退出登录
如果默认的退出登录无法满足,可以自定义处理器来解决。
3.2.1、自定义 LogoutHandler
默认情况下清除认证信息 (clearAuthentication),和Session 失效(invalidateHttpSession
) 已经由内置的SecurityContextLogoutHandler
来完成。
这个 LogoutHandle 主要用来处理用户信息。
/** * 登出接口处理器 */public class MyLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { User user = (User) authentication.getPrincipal(); //执行用户信息操作,如记录用户下线时间... }}
3.2.2、自定义 LogoutSuccessHandler
这个 LogoutSuccessHandler 用于返回响应信息给前端,可以返回 json、重定向页面。
注意配置这个处理器之后,就不需要配置 logoutSuccessUrl
了。
/** * 登出成功处理器 */public class MyLogoutSuccessHandler implements LogoutSuccessHandler { private String url; public MyLogoutSuccessHandler(String url) { this.url = url; } @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //重定向 response.sendRedirect(url); }}
3.3.3、spring security 添加配置
@Overrideprotected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义用户名和密码参数 .usernameParameter("username123") .passwordParameter("password123") //自定义登录页面 .loginPage("/login.html") //必须和表单提交的接口一样,执行自定义登录逻辑 .loginProcessingUrl("/login") //自定义登录成功处理器 .successHandler(new MyAuthenticationSuccessHandler("/main.html")) //自定义登录失败处理器 .failureHandler(new MyAuthenticationFailureHandler("/error.html")); //授权 http.authorizeRequests() //放行/login.html,不需要认证 .antMatchers("/login.html").permitAll() //放行/error.html,不需要认证 .antMatchers("/error.html").permitAll() //基于权限判断 .antMatchers("/main1.html").hasAuthority("permission1") //所有请求必须认证 .anyRequest().authenticated(); //异常处理器 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); //登出 http.logout() //登出接口,与表单访问接口一致 .logoutUrl("/signLogout") //登出处理器 .addLogoutHandler(new MyLogoutHandler()) //登出成功后跳转的页面 .logoutSuccessHandler(new MyLogoutSuccessHandler("/login.html")); //关闭csrf防护 http.csrf().disable();}
3.3.4、修改登出接口
main.html 修改如下:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Title</title></head><body>登录成功!!!<a href="/signLogout">退出</a><a href="/main1.html">跳转权限页</a></body></html>
运行测试后,返回 localhost://8080/login.html
4、基于注解的权限控制
4.1、权限注解参数
关于权限的注解参数共有三个:
- @PreAuthorize:方法执行前进行权限检查
- @PostAuthorize:方法执行后进行权限检查
- @Secured:类似于 @PreAuthorize
4.2、启动类添加 @EnableGlobalMethodSecurity
启动类配置如下:
@SpringBootApplication@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)public class SpringSecurityStudyApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityStudyApplication.class, args); }}
4.3、运行测试
4.3.1、修改 spring security 和 自定义登录逻辑
successHander(登录成功处理器) 修改为 successForwardUrl(登录成功访问路径),删除 permission1的权限判断,改成访问接口时进行权限判断。
@Overrideprotected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义用户名和密码参数 .usernameParameter("username123") .passwordParameter("password123") //自定义登录页面 .loginPage("/login.html") //必须和表单提交的接口一样,执行自定义登录逻辑 .loginProcessingUrl("/login") //登录成功跳转的页面,post请求 .successForwardUrl("/toMain") //自定义登录失败处理器 .failureHandler(new MyAuthenticationFailureHandler("/error.html")); //授权 http.authorizeRequests() //放行/login.html,不需要认证 .antMatchers("/login.html").permitAll() //放行/error.html,不需要认证 .antMatchers("/error.html").permitAll() //所有请求必须认证 .anyRequest().authenticated(); //异常处理器 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); //登出 http.logout() //登出接口,与表单访问接口一致 .logoutUrl("/signLogout") //登出处理器 .addLogoutHandler(new MyLogoutHandler()) //登出成功后跳转的页面 .logoutSuccessHandler(new MyLogoutSuccessHandler("/login.html")); //关闭csrf防护 http.csrf().disable();}
自定义登录逻辑如下:
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //校验用户名,实际环境中需要从数据库查询 if (!username.equals("admin")) { throw new UsernameNotFoundException("用户不存在"); } //比较密码,实际需要从数据库取出原密码校验,框架会自动读取登录页面的密码 String password = bCryptPasswordEncoder.encode("123456"); //返回UserDetails,实际开发中可拓展UserDetails return new User(username, password, //自定义权限 AuthorityUtils.commaSeparatedStringToAuthorityList("permission1"));}
4.3.2、添加测试接口
//登录成功跳转页@PostMapping("/toMain")//判断是否拥有permission1的权限@PreAuthorize("hasPermission('permission1')")public String toMain() { //获得认证用户信息 Object object = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (object instanceof UserDetails) { //进行一系列操作 } return "redirect:main.html";}
4.3.3、运行测试
登录成功,通过 /toMain
接口重定向到 main.html
:
5、自定义登录过滤器
当默认的 UserDetailsService
无法满足需求,例如增加图形验证码校验,此时可以自己创建登录过滤器 UsernamePasswordAuthenticationFilter 及登录认证处理 AuthenticationProvider,对业务逻辑进行拓展。
5.1、自定义身份认证处理
实现 AuthenticationProvider
/** * 登录认证处理 * @author Lin */public class MyAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); //校验账号,实际需要从DB查询 if (!username.equals("admin")) { throw new UsernameNotFoundException("用户不存在"); } //校验密码,实际需要加密并与DB的password比较 if (!password.equals("123456")) { throw new InternalAuthenticationServiceException("密码错误"); } return new UsernamePasswordAuthenticationToken(username, password, //自定义权限 AuthorityUtils.commaSeparatedStringToAuthorityList("permission1,ROLE_abc,/main.html")); } @Override public boolean supports(Class<?> authentication) { return true; }}
5.2、自定义登录过滤器
实现 UsernamePasswordAuthenticationFilter
/** * 登录过滤器 * @author Lin */public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public MyUsernamePasswordAuthenticationFilter() { //登录成功处理器 this.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler("/main.html")); //登录失败处理器 this.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler("/error.html")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = request.getParameter("username123"); String password = request.getParameter("password123"); if (Strings.isBlank(username)) { throw new AuthenticationServiceException("账户不能为空"); } if (Strings.isBlank(password)) { throw new AuthenticationServiceException("密码不能为空"); } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); setDetails(request,authenticationToken); return new MyAuthenticationProvider().authenticate(authenticationToken); }}
5.3、修改spring security 的配置类
中间代码忽略,改动部分如下,增加自定义登录过滤器的配置
@Overrideprotected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义用户名和密码参数 .usernameParameter("username123") .passwordParameter("password123") //自定义登录页面 .loginPage("/login.html") //必须和表单提交的接口一样,执行自定义登录逻辑 .loginProcessingUrl("/login") .... .... //自定义登录过滤器 http.addFilterBefore(new MyUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
运行测试后,debug
发现账号认证走的自定义登录过滤器 MyUsernamePasswordAuthenticationFilter
,说明配置成功。
6、基于动态权限的权限控制
上文演示了基于注解的权限控制,但在实际的开发中需要做到动态权限控制,spring security能够支持动态权限,需要实现 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager。
6.1、自定义权限过滤器
权限过滤器实现 FilterInvocationSecurityMetadataSource,用于返回访问 url 所需的权限。
注意:在添加权限过滤器后,所有的请求都会经过该过滤器,包括登录页面/login.html
,即使 permitAll()
,因此需要添加匿名角色ROLE_ANONYMOUS
映射登录页面 login.html
,如下面代码的put("/login.html", "ROLE_ANONYMOUS")
。
/** * 权限过滤器 * * @author Lin * @Description 返回url需要的权限 */@Componentpublic class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static final Logger log = LoggerFactory.getLogger(MyFilterInvocationSecurityMetadataSource.class); @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { //获取请求路径 String requestUrl = ((FilterInvocation) object).getRequestUrl(); log.info("==========requestUrl:" + requestUrl); // 这里的需要从DB加载 AntPathMatcher antPathMatcher = new AntPathMatcher(); Map<String, String> urlRoleMap = new HashMap<String, String>() {{ //登录页为匿名角色访问 put("/login.html", "ROLE_ANONYMOUS"); put("/toMain", "ROLE_USER"); }}; List<String> roleList = new ArrayList<>(); int i = 0; for (Map.Entry<String, String> entry : urlRoleMap.entrySet()) { if (antPathMatcher.match(entry.getKey(), requestUrl)) { roleList.add(entry.getValue()); } } List<ConfigAttribute> configAttributes = SecurityConfig.createList(roleList.toArray(new String[roleList.size()])); return configAttributes; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); }}
6.2、自定义权限决策管理器
权限决策管理器实现 AccessDecisionManager,判断用户是否有访问 url 的权限。
/** * 权限决策管理器 * @author Lin * @Description 根据url判断用户是否有访问权限 */@Componentpublic class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { String needPermission = iterator.next().getAttribute(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needPermission)){ return; } } } throw new AccessDeniedException("权限不足"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; }}
6.3、修改 sprIng security 的配置类
@Overrideprotected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义用户名和密码参数 .usernameParameter("username123") .passwordParameter("password123") //自定义登录页面 .loginPage("/login.html") //必须和表单提交的接口一样,执行自定义登录逻辑 .loginProcessingUrl("/login"); //授权 http.authorizeRequests() //动态权限控制 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource); object.setAccessDecisionManager(myAccessDecisionManager); return object; } }) //放行/login.html,不需要认证 .antMatchers("/login.html").permitAll() //放行/error.html,不需要认证 .antMatchers("/error.html").permitAll(); //异常处理器 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); //登出 http.logout() //登出接口,与表单接口一致 .logoutUrl("/signLogout") //登出处理器 .addLogoutHandler(new MyLogoutHandler()) //登出成功后跳转的页面 .logoutSuccessHandler(new MyLogoutSuccessHandler("/login.html")); //关闭csrf防护 http.csrf().disable(); //自定义登录过滤器 http.addFilterBefore(new MyUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
这样动态权限控制便配置完成。
7、参考资料
https://www.bilibili.com/video/BV1Cz4y1k7rd?from=search&seid=8886448532131988851
https://blog.csdn.net/zhaoxichen_10/article/details/88713799