2.Spring Security 配置多用户认证
概述
认证就是登陆,我们现在没有连接数据库,那么我们可以模拟下用户名和密码
/** * @Author 杨不易呀 * web 安全的配置类 * <p> * WebSecurityConfigurerAdapter web安全配置的适配器 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置认证(用户)管理 模拟内存用户数据 * 重点说明: * 在开发中,我们一般只针对权限,很少去使用角色 * 后面的讲解中我们以权限为主也就是 authorities 这里面的东西 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 在内存中创建了两个用户 // 注意点: 我们添加了安全配置类,那么我们在 yml 里面的用户密码配置就失效了 auth.inMemoryAuthentication() .withUser("yby") // 用户名 .password("yby") // 密码 .roles("ADMIN_yby") // 给了一个角色 .authorities("sys:add", "sys:update", "sys:delete", "sys:select") // 注意点:给yby用户四个权限 如果权限和角色都给了 那么角色就失效了 .and() .withUser("test") .password("test") .roles("TEST") .authorities("sys:select") // 加了一个权限 ; } }
1.启动测试
使用yby/yby登录访问 可以发现 控制台报错了
这个是因为 spring Sercurity 强制要使用密码加密,当然我们也可以不加密,但是官方要求是不 管你是否加密,都必须配置一个类似 Shiro 的凭证匹配器
2.添加密码加密器
/** * 给容器中放一个加密器 springSecurity5.x之后推荐使用加密 * 也可以不给加密 * new NoOpPasswordEncoder() * 这个加密器对同一个值加密后 会得到不同的结果 * 只要是用同一个加密器加密的 解密也是一样的 * * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
3.修改用户配置
// 在内存中创建了两个用户 // 注意点: 我们添加了安全配置类,那么我们在 yml 里面的用户密码配置就失效了 auth.inMemoryAuthentication() .withUser("yby") // 用户名 .password(passwordEncoder().encode("yby") // 密码 .roles("ADMIN_yby") // 给了一个角色 .authorities("sys:add", "sys:update", "sys:delete", "sys:select") // 注意点:给yby用户四个权限 如果权限和角色都给了 那么角色就失效了 .and() .withUser("test") .password(passwordEncoder().encode("test") .roles("TEST") .authorities("sys:select") ;
4.重启测试
两个用户都可以登录成功l了 恭喜恭喜!!!!
5.测试加密和解密 demo
public class TestPasswordEncoder { public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode1 = passwordEncoder.encode("123"); System.out.println(encode1); String encode2 = passwordEncoder.encode("123"); System.out.println(encode2); String encode3 = passwordEncoder.encode("123"); System.out.println(encode3); // 查看加密后是否匹配 System.out.println(passwordEncoder.matches("123", encode1)); System.out.println(passwordEncoder.matches("123", encode2)); System.out.println(passwordEncoder.matches("123", encode3)); } }
查看控制台发现特点是:相同的字符串加密之后的结果都不一样,但是比较的时候是一样的,这个 算法比 shiro 的 MD5 好用,不用自己在数据库去存盐了
3.如何获取当前登录用户的信息(两种方式)
1.往HelloController添加请求
/** * 获取当前用户信息,直接在参数中注入 Principal 对象 * 此对象是登录后自动写入 UsernamePasswordAuthenticationToken 类中 * * @param principal * @return */ @GetMapping("userInfo") public Principal getUserInfo(Principal principal) { return principal; } /** * SecurityContextHolder.getContext()获取安全上下文对象 * 就是那个保存在 ThreadLocal 里面的安全上下文对象 * 总是不为 null(如果不存在,则创建一个 authentication 属性为 null 的 empty 安全上下文对象) * 获取当前认证了的 principal(当事人),或者 request token (令牌) * 如果没有认证,会是 null,该例子是认证之后的情况 */ @GetMapping("userInfo2") public Object getUserInfo2() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); ret
2.请求任意一个都可以获取到登陆后的json信息
4.Spring Security 用户,角色,权限拦截配置讲解
1.角色和权限的配置,修改 WebSecurityConfig 类
/** * 配置认证(用户)管理 模拟内存用户数据 * 重点说明: * 在开发中,我们一般只针对权限,很少去使用角色 * 后面的讲解中我们以权限为主也就是 authorities 这里面的东西 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 在内存中创建了两个用户 auth.inMemoryAuthentication() .withUser("yby") // 用户名 .password(passwordEncoder().encode("yby")) // 密码 需要加密 .roles("ADMIN_SXT") // 给了一个角色 .authorities("sys:add", "sys:update", "sys:delete", "sys:select") // 给yby用户四个权限 如果权限和角色都给了 那么角色就失效了 // 测试用户 只有一个查看权限 .and() .withUser("test") .password(passwordEncoder().encode("test")) .roles("TEST") // 失效 因为有 authorities这个了 .authorities("sys:select") // admin用户 角色权限区分 .and() .withUser("admin") .password(passwordEncoder().encode("admin")) .roles("ADMIN") // 注意点:不能带入前缀ROLE_ security里面默认会添加的 最终结果是 ROLE_ADMIN ; } /** * 配置 http 请求验证等 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 注释掉他自己的方法 走我们自己的 // super.configure(http); // 给一个表单登陆 就是我们的登录页面,登录成功或者失败后走我们的 url http.formLogin() .successForwardUrl("/welcome") // 登录成功走的url .failureForwardUrl("/fail") // 登录失败走的url .permitAll(); // 匹配哪些 url,需要哪些权限才可以访问 当然我们也可以使用链式编程的方式 http.authorizeRequests() .antMatchers("/query").hasAuthority("sys:query") // 表示这个用户有这个权限标识才能访问 .antMatchers("/save").hasAuthority("sys:save") .antMatchers("/del").hasAuthority("sys:del") .antMatchers("/update").hasAuthority("sys:update") .antMatchers("/admin/**").hasRole("ADMIN") // 表示这个用户有这个角色才能访问 ; // 其他所有的请求都需要登录才能进行 // 所有的请求都需要认证才可以访问 http.authorizeRequests().anyRequest().authenticated(); }
2.创建AuthorityController 演示权限访问
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author 杨不易呀 */ @RestController public class AuthorityController { /** * 登录成功的主页返回值 * * @return */ @PostMapping("welcome") public String welcome() { return "欢迎来到主页"; } /** * 登录失败的返回值 * * @return */ @PostMapping("fail") public String fail() { return "登录失败了"; } /** * 开启方法权限的注解 * * @return */ @GetMapping("add") public String add() { return "欢迎来到主ADD"; } @GetMapping("update") public String update() { return "欢迎来到UPDATE"; } @GetMapping("delete") public String delete() { return "欢迎来到DELETE"; } @GetMapping("select") public String select() { return "欢迎来到SELECT"; } @GetMapping("role") public String role() { return "欢迎来到ROLE"; } @GetMapping("admin/hello") public String admin() { return "我是只有 admin 角色才可以访问的"; } }
3.创建访问403权限不足页面.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>没有访问权限哦</title> </head> <body> <h1 style="color: red">您没有访问权限</h1> </body> </html>
会自动的跳转到该目录下.
访问该用户没有的权限请求
5.Spring Security 返回 JSON(前后端分离)
在上面的例子中,我们返回的是 403 页面,但是在开发中,如 RestAPI 风格的数据,是不能返回一 个页面,而应该是给一个 json
1.添加处理器 RestAuthorizationAccessDeniedHandler
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; /** * @Author 杨不易呀 * 自定义登录成功的处理器 * 返回json */ @Configuration public class AuthenticateSuccess implements AuthenticationSuccessHandler { /** * 登陆成功后执行的处理器 * * @param request * @param response * @param authentication * @throws IOException * @throws ServletException */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("登录成功了"); // 把json串写出去 response.setContentType("application/json;charset=utf-8"); HashMap<String, Object> map = new HashMap<>(8); map.put("code", 200); map.put("msg", "登录成功"); // 把用户信息返回给前端 让前端可以保存起来 map.put("data", authentication); ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(map); // 写出去 PrintWriter writer = response.getWriter(); writer.write(s); // 刷新流 关闭流 writer.flush(); writer.close(); } }
2.修改WebSecurityConfig配置文件
/** * 将自定义的拒绝访问处理器注入进来 */ @Autowired private AccessDeniedHandler accessDeniedHandler; /** * 配置 http 请求验证等 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 自定义403请求返回json http.exceptionHandling().accessDeniedHandler(accessDeniedHandler()); // 给一个表单登陆 就是我们的登录页面,登录成功或者失败后走我们的 url http.formLogin() .successForwardUrl("/welcome") // 登录成功走的url .failureForwardUrl("/fail") // 登录失败走的url .permitAll(); // 匹配哪些 url,需要哪些权限才可以访问 当然我们也可以使用链式编程的方式 http.authorizeRequests() .antMatchers("/query").hasAuthority("sys:query") // 表示这个用户有这个权限标识才能访问 .antMatchers("/add").hasAuthority("sys:add") .antMatchers("/delete").hasAuthority("sys:delete") .antMatchers("/update").hasAuthority("sys:update") .antMatchers("/admin/**").hasRole("ADMIN") // 表示这个用户有这个角色才能访问 ; // 其他所有的请求都需要登录才能进行 // 所有的请求都需要认证才可以访问 http.authorizeRequests().anyRequest().authenticated(); }
3.重新启动访问用户没有权限的url下·
4.登录成功或者失败都返回 JSON,我们需要自定义处理器
创建 AuthenticateSuccess
登录成功返回json
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; /** * @Author 杨不易呀 * 自定义登录成功的处理器 * 返回json */ @Configuration public class AuthenticateSuccess implements AuthenticationSuccessHandler { /** * 登陆成功后执行的处理器 * * @param request * @param response * @param authentication * @throws IOException * @throws ServletException */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("登录成功了"); // 把json串写出去 response.setContentType("application/json;charset=utf-8"); HashMap<String, Object> map = new HashMap<>(8); map.put("code", 200); map.put("msg", "登录成功"); // 把用户信息返回给前端 让前端可以保存起来 map.put("data", authentication); ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(map); // 写出去 PrintWriter writer = response.getWriter(); writer.write(s); // 刷新流 关闭流 writer.flush(); writer.close(); } }
修改配置 http 请求验证
注入登录失败和登录成功返回json
/** * 自定义登录成功返回json */ @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; /** * 配置 http 请求验证等 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 给一个表单登陆 就是我们的登录页面,登录成功或者失败后走我们的 url //http.formLogin() // .successForwardUrl("/welcome") // 登录成功走的url //.failureForwardUrl("/fail") // 登录失败走的url // .permitAll(); // 这里使用了前后端分离的模式 实现我们的登录成功和失败返回json http.formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler()); // 匹配哪些 url,需要哪些权限才可以访问 当然我们也可以使用链式编程的方式 http.authorizeRequests() .antMatchers("/query").hasAuthority("sys:query") // 表示这个用户有这个权限标识才能访问 .antMatchers("/save").hasAuthority("sys:save") .antMatchers("/del").hasAuthority("sys:del") .antMatchers("/update").hasAuthority("sys:update") .antMatchers("/admin/**").hasRole("ADMIN") // 表示这个用户有这个角色才能访问 ; // 其他所有的请求都需要登录才能进行 // 所有的请求都需要认证才可以访问 http.authorizeRequests().anyRequest().authenticated(); } /** * 登录失败的处理器 * * @return */ @Bean public AuthenticationFailureHandler authenticationFailureHandler() { return (request, response, exception) -> { response.setContentType("application/json;charset=utf-8"); System.out.println(exception); // 有很多登录失败的异常 HashMap<String, Object> map = new HashMap<>(4); map.put("code", 401); // instanceof 判断左右是否是右边的 一个实例 这里的exception已经是一个具体的错误了 if (exception instanceof LockedException) { map.put("msg", "账户被锁定,登陆失败!"); } else if (exception instanceof BadCredentialsException) { map.put("msg", "账户或者密码错误,登陆失败!"); } else if (exception instanceof DisabledException) { map.put("msg", "账户被禁用,登陆失败!"); } else if (exception instanceof AccountExpiredException) { map.put("msg", "账户已过期,登陆失败!"); } else if (exception instanceof CredentialsExpiredException) { map.put("msg", "密码已过期,登陆失败!"); } else { map.put("msg", "登陆失败!"); } ObjectMapper objectMapper = new ObjectMapper(); String s = objectMapper.writeValueAsString(map); PrintWriter writer = response.getWriter(); writer.write(s); writer.flush(); writer.close(); }; }
5.重新启动工程进行登录测试json返回
6.Spring Security 方法授权 权限访问限制
我们使用方法级别的授权后,只需要在 controller 对应的方法上添加注解即可了,不需要再 webSecurityConfig 中配置匹配的 url 和权限了,这样就爽多了
1.相关注解说明
@PreAuthorize 在方法调用前进行权限检查
@PostAuthorize 在方法调用后进行权限检查
@Secured 上面的三个注解如果要使用的话必须加上
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
如果只使用 PreAuthorize 就只用开启 prePostEnabled = true
如果只使用@Secured 就只用开启 securedEnabled = true 这种方式不推荐,有坑 坑在这里 @Secured,而@Secured 对应的角色必须要有 ROLE_前缀
2.在 WebSecurityConfig 类或者启动类上添加注解
3.注释掉 WebSecurityConfig 配置 url 和权限的代码
4.修改 controller,给方法添加注解
不加注解的,都可以访问,加了注解的,要有对应权限才可以访问哦
/** * 开启方法权限的注解 * * @return */ @GetMapping("add") @PreAuthorize("hasAuthority('sys:add')") public String add() { return "欢迎来到主ADD"; } @GetMapping("update") @PreAuthorize("hasAuthority('sys:update')") public String update() { return "欢迎来到UPDATE"; } @GetMapping("delete") @PreAuthorize("hasAuthority('sys:delete')") public String delete() { return "欢迎来到DELETE"; } @GetMapping("select") @PreAuthorize("hasAuthority('sys:select')") public String select() { return "欢迎来到SELECT"; } @GetMapping("role") public String role() { return "欢迎来到ROLE"; }