SpringBoot Security从入门到实战示例教程

网友投稿 332 2022-07-31


目录前言入门测试接口增加依赖自定义配置配置密码加密方式配置AuthenticationManagerBuilder 认证用户、角色权限配置HttpSecurity Url访问权限自定义successHandler自定义failureHandler自定义未认证处理自定义权限不足处理自定义注销登录前后端分离场景提供登录接口自定义认证过滤器鉴权1.注解鉴权2.自定义Bean动态鉴权3.扩展默认方法自定义扩展根对象SecurityExpressionRoot登出跨域全局配置

前言

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。它的核心是一组过滤器链,不同的功能经由不同的过滤器。这篇文章给大家讲解SpringBoot Security从入门到实战,内容如下所示:

入门

测试接口

假设我们用下面的接口做权限测试。

@RestController

public class LakerController {

@GetMapping("/laker")

public String laker() {

return IdUtil.simpleUUID();

}

@GetMapping("/laker/q")

public String lakerQ() {

return IdUtil.simpleUUID();

}

}

浏览器访问:http://localhost:8080/laker,结果如下:

增加依赖

在 pom.xml,添加 spring-boot-starter-securtiy 依赖。

org.springframework.boot

spring-boot-starter-security

再次访问http://localhost:8080/laker,结果如下:

简要解析

我们访问http://localhost:8080/laker,security判断我们没有登录,则会302重定向到http://localhost:8080/login(默认)

security会返回一个默认的登录页。默认用户名为:user,密码在服务启动时,会随机生成一个,可以查看启动日志如下:

2022-05-02 21:01:03.697  INFO 17896 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'2022-05-02 21:01:03.825  INFO 17896 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: e53fef6a-3f61-43c3-9609-ce88fd7c0841

当然,可以通过配置文件设置默认的用户名、密码、角色。

spring:

security:

user:

# 默认是 user

name: laker

password: laker

roles:

- ADMIN

- TESTER

以上的默认都是可以修改的哈。

判断是否登录使用传统的cookie session模式哈,jsESSIONID E3512CD1A81DB7F2144C577BA38D2D92 HttpOnly true

自定义配置

实际项目中我们的用户、密码、角色、权限、资源都是存储在数据库中的,我们可以通过自定义类继承 WebSecurityConfigurerAdapter,从而实现对 Spring Security 更多的自定义配置。

@Configuration

public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

...

}

配置密码加密方式

必须配置,否则报空指针异常。

@Bean

PasswordEncoder passwordEncoder() {

// 不加密

return NoOpPasswordEncoder.getInstance();

}

Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder:BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 取值在 4~31 之间(默认为 10)。strength 越大,密钥的迭代次数越多(密钥迭代次数为 2^strength)。如果是数据库认证,库里的密码同样也存放加密后的密码。同一密码每次 Bcrypt 生成的结果都会变化不会重复。

配置AuthenticationManagerBuilder 认证用户、角色权限

支持直接配置内存认证模式和配置UserDetailsServiceBean方式

内存认证模式,实际项目不用这个哦。(仅做了解)

/**

* 配置用户及其对应的角色

*/

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()

.withUser("admin").password("123").roles("ADMIN", "USER")

.and()

.withUser("laker").password("123").roles("USER");

}

上面在UserDetailsService的实现之一InMemoryUserDetailsManager中,即存储在内存中。

自定义UserDetailsServiceBean方式,实际项目都是使用这个,可定制化程度高。

步骤一:定义一个LakerUserService实现UserDetailsService,配置成SpringBean。该方法将在用户登录时自动调用。

@Service

public class LakerUserService implements UserDetailsService {

@Autowired

UserMapper userMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// username 就是前端传递的例如 laker 123,即 laker

User user = userMapper.loadUserByUsername(username);

if (user == null) {

throw new UsernameNotFoundException("账户不存在!");

}

user.setAuthorities(...);

return user;

}

}

username

password(加密后的密码)

authorities

返回的Bean中以上3个都不能为空。返回User后由系统提供的 DaoAuthenticationProvider 类去比对密码是否正确。

Spring Security默认支持表单请求登录的源码,UsernamePasswordAuthenticationFilter.java

步骤二:在把自定义的LakerUserService装载进去.

@Autowired

UserService userService;

...

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(userService);

}

步骤三:其中我们的业务用户User必须要实现UserDetails接口,并实现该接口中的 7 个方法:

getAuthorities():获取当前用户对象所具有的权限信息getPassword():获取当前用户对象的密码

返回的密码和用户输入的登录密码不匹配,会自动抛出 BadCredentialsException 异常。

getUsername():获取当前用户对象的用户名isAccountNonExpired():当前账户是否未过期isAccountNonLocked():当前账户是否未锁定返回了 false,会自动抛出 AccountExpiredException 异常。isCredentialsNonExpired():当前账户密码是否未过期isEnabled():当前账户是否可用

@Data

public class User implements UserDetails {

private Integer id;

private String username;

private String password;

private Boolean enabled;

private Boolean locked;

private List authorities;

@Override

public String getPassword() {

return password;

}

@Override

public String getUsername() {

return username;

}

@Override

public boolean isAccountNonExpired() {

return true;

}

@Override

public boolean isAccountNonLocked() {

return !locked;

}

@Override

public boolean isCredentialsNonExpired() {

return true;

}

@Override

public boolean isEnabled() {

return enabled;

}

@Override

public Collection extends GrantedAuthority> getAuthorities() {

List authoritiesList = new ArrayList<>();

for IHTjqITcYl(String authority : authorities) {

authoritiesList.add(new SimpleGrantedAuthority(authority));

}

return authoritiesList;

}

}

配置HttpSecurity Url访问权限

/**

* 配置 URL 访问权限

*/

@Override

protected void configure(HttpSecurity http) throws Exception {

//

http // 1.开启 HttpSecurity 配置

.authorizeRequests()

// laker/** 模式URL必须具备laker.query

.antMatchers("/laker/**").hasAnyAuthority("laker.query")

// 用户访问其它URL都必须认证后访问(登录后访问)

.anyRequest().authenticated()

.and()

// 2.开启表单登录,前后端分离的时候不用这个

.formLogin()

// 未登录时 重定向的url 默认是/login 内置的页面,可以自己自定义哈。一般前后端分离,不用这个

// .loginPage("/login")

//

// .defaultSuccessUrl("/user",true)

// .usernameParameter("username") // default is username

// .passwordParameter("password") // default is password

// .loginPage("/authentication/login") // default is /login with an HTTP get

// .failureUrl("/authentication/login?failed") // default is /login?error

// .loginProcessingUrl("/authentication/login/process") // default is /login

.and()

// 3.关闭csrf,前后端分离不需要这个。

.csrf().disable();

//授权码模式需要 会弹出默认自带的登录框

http.httpBasic();

// 开启注销登录的配置

http.logout()

// 配置注销登录请求URL为"/logout"(默认也就是 /logout)

.logoutSuccessUrl("/logout")

.clearAuthentication(true) // 清除身份认证信息

.invalidateHttpSession(true) // 使 session 失效;

}

formLogin() 表示开启表单登录**defaultSuccessUrl()**表示默认登录验证成功跳转的url,默认重定向到上次访问未成功的,如果没有则重定向到/.loginProcessingUrl() 方法配置登录接口为“/login”,即可以直接调用“/login”接口,发起一个 POST 请求进行登录,登录参数中用户名必须为 username,密码必须为 password,配置 loginProcessingUrl 接口主要是方便 Ajax 或者移动端调用登录接口。

anyRequest          |   匹配所有请求路径    access              |   SpringEl表达式结果为true时可以访问    anonymous           |   匿名可以访问 所有人都能访问,但是带上 token访问后会报错403    denyAll             |   用户不能访问 所有人都能访问,包括带上 token 访问    fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)    hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问    hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问    hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问    hasIpAddress &http://nbsp;      |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问    hasRole             |   如果有参数,参数表示角色,则其角色可以访问    permitAll           |   用户可以任意访问    rememberMe          |   允许通过remember-me登录的用户访问    authenticated       |   用户登录后可访问

自定义successHandler

登录成功后默认是重定向url,我们可以自定义返回json用于前后端分离场景以及其他逻辑,例如成功之后发短信等。

http.formLogin().successHandler((req, resp, authentication) -> {

// 发短信哈

Object principal = authentication.getPrincipal();

resp.setContentType("application/json;charset=utf-8");

PrintWriter out = resp.getWriter();

out.write(new ObjectMapper().writeValueAsString(principal));

out.flush();

out.close();

})

自定义failureHandler

登录失败回调

http.formLogin().failureHandler((req, resp, e) -> {

resp.setContentType("application/json;charset=utf-8");

PrintWriter out = resp.getWriter();

out.write(e.getMessage());

out.flush();

out.close();

})

自定义未认证处理

http.exceptionHandling()

.authenticationEntryPoint((req, resp, authException) -> {

resp.setContentType("application/json;charset=utf-8");

PrintWriter out = resp.getWriter();

out.write("尚未登录,请先登录");

out.flush();

out.close();

}

);

自定义权限不足处理

http.exceptionHandling()

//没有权限,返回json

.accessDeniedHandler((request,response,ex) -> {

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_FORBIDDEN);

PrintWriter out = response.getWriter();

Map map = new HashMap();

map.put("code",403);

map.put("message", "权限不足");

out.write(objectMapper.writeValueAsString(map));

out.flush();

out.close();

})

自定义注销登录

.logout()

.logoutUrl("/logout")

.logoutSuccessHandler((req, resp, authentication) -> {

resp.setContentType("application/json;charset=utf-8");

PrintWriter out = resp.getWriter();

out.write("注销成功");

out.flush();

out.close();

})

前后端分离场景

上面的都是入门的,实际项目中一般都是前后端分离的,在登录时都是自定义登录接口,例如登录接口是restful风格,增加了其他的验证码参数,还使用jwt来完成登录鉴权等。

提供登录接口

该接口需要在配置当中放行,未授权访问需要授权的请求时,会返回401或者403状态码,前端可以根据这个进行路由提示处理。

@RestController

public class LoginController {

@Autowired

LoginService ...

@PostMapping("/login")

public login(@RequestBody Login login){

...

return token;

}

}

Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象.

@Service

public class LoginServiceImpl implements LoginService {

@Autowired

private AuthenticationManager authenticationManager;

@Autowired

private UserDetailsService userDetailsService;

@Override

public doLogin(Login login) {

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password;

Authentication authenticate

try { // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername

authenticate = authenticationManager.authenticate(authenticationToken);

} catch (AuthenticationException e) {

e.printStackTrace();

}

if (Objects.isNull(authenticate)) {

//用户名密码错误

throw new ServicesException(...);

}

User authUser = (User) authenticate.getPrincipal();

String token = JwtUtil.createJWT(username);

Map map = new HashMap<>();

map.put("token", token);

return map;

}

}

自定义认证过滤器

坊间有2种实现方式。

方式一:继承UsernamePasswordAuthenticationFilter的写法需要使用登陆成功处理器、失败处理器等,还是需要按照security这一套来玩。

Spring Security默认支持表单请求登录的源码UsernamePasswordAuthenticationFilter.java

方式二:使用Filter的写法没有任何限制怎么玩都行,比如说添加其他参数验证码,返回json,token鉴权等。

@Component

public class LakerOncePerRequestFilter extends OncePerRequestFilter {

@Override

protected void doFilterInternal(HttpServletRequest request,

HttpServletResponse response,

FilterChain chain) throws ServletException, IOException {

String token = request.getHeader("Authorization");

if (!StringUtils.isEmpty(token) )

{

// 校验token ...

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities;

authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken);

}

chain.doFilter(request, response);

}

} //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器

http.addFilterBefore(lakerOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);

鉴权

1.注解鉴权

在SpringSecurity配置类中开启方法级的认证使用 @PreAuthorize注解在方法或者类

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

...

@RestController

public class Controller {

@GetMapping("/hello")

@PreAuthorize("hasAnyAuthority('laker.query')")

public String test() {

}

2.自定义Bean动态鉴权

因为@PreAuthorize支持SpringEL表达式,所以可以支持自定义SpringBean动态鉴权。

先自定义一个SpringBean。使用 @PreAuthorize注解在方法或者类配合@PreAuthorize(“@rbacService.hasPermission(‘xx’)”)

@Component("rbacService")

public class LakerRBACService {

public boolean hasPermission(HttpServletRequest request, Authentication authentication) {

Object principal = authentication.getPrincipal();

if (principal instanceof UserDetails) {

UserDetails userDetails=(UserDetails)principal;

/**

* 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true

*/

//本次要访问的资源

SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getMethod() + "" + request.getRequestURI());

//用户拥有的权限中是否包含请求的url

return userDetails.getAuthorities().contains(simpleGrantedAuthority);

}

return false;

}

public boolean hasPermission() {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

Object principal = authentication.getPrincipal();

if (principal instanceof UserDetails) {

UserDetails userDetails = (UserDetails) principal;

/**

* 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true

*/

//本次要访问的资源

HttpServletRequest request =((ServletRequestAttributes)

RequestContextHolder.getRequestAttributes()).getRequest();

SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());

//用户拥有的权限中是否包含请求的url

return userDetails.getAuthorities().contains(simpleGrantedAuthority);

}

return false;

}

}

// controller方法

@PreAuthorize("@rbacService.hasPermission()")

public String test() {

}

// 或者高级的全局url鉴权

public class SecurityConfig extends WebSecurityConfigurerAdapter {

...

http.authorizeRequests() //设置授权请求,任何请求都要经过下面的权限表达式处理

.anyRequest().access("@rbacService.hasPermission(request,authentication)") //权限表达式

3.扩展默认方法自定义扩展根对象SecurityExpressionRoot

https://jb51.net/article/245172.htm

1.创建自定义根对象

public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

public CustomMethodSecurityExpressionRoot(Authentication authentication) {

super(authentication);

}

/**

* 自定义表达式

* @param username 具有权限的用户账号

*/

public boolean hasUser(String... username) {

String name = this.getAuthentication().getName();

HttpServletRequest request = ((ServletRequestAttributes)

RequestContextHolder.getRequestAttributes()).getRequest();

String[] names = username;

for (String nameStr : names) {

if (name.equals(nameStr)) {

return true;

}

}

return false;

}

}

2.创建自定义处理器

创建自定义处理器,主要是重写创建根对象的方法。

public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

@Override

protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {

CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);

root.setThis(invocation.getThis());

root.setPermissionEvaluator(getPermissionEvaluator());

root.setTrustResolver(getTrustResolver());

root.setRoleHierarchy(getRoleHierarchy());

root.setDefaultRolePrefix(getDefaultRolePrefix());

return root;

}

}

3.配置GlobalMethodSecurityConfiguration之前我们使用@EnableGlobalMethodSecurity开启全局方法安全,而这些全局方法级别的安全配置就在GlobalMethodSecurityConfiguration配置类中。

可以扩展这个类来自定义默认值,但必须确保在类上指定@EnableGlobalMethodSecurity 注解,否则会bean冲突报错。

@Configuration

// 将EnableGlobalMethodSecurity注解移到这里

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

@Override

protected MethodSecurityExpressionHandler createExpressionHandler() {

return new CustomMethodSecurityExpressionHandler();

}

}

4.controller使用自定义方法

@PreAuthorize("hasUser('laker','admin')")

public String test() {

...

}

登出

http.logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> {

// 删除用户token

...

// 返回json

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_OK);

PrintWriter out = responIHTjqITcYlse.getWriter();

out.write("OK");

out.flush();

out.close();

});

跨域

@Override

protected void configure(HttpSecurity http) throws Exception {

http.cors();//允许跨域,配置后SpringSecurity会自动寻找name=corsConfigurationSource的Bean

http.csrf().disable();//关闭CSRF防御

}

@Configuration

public class CrosConfig {

@Bean

CorsConfigurationSource corsConfigurationSource() {

final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

CorsConfiguration cores=new CorsConfiguration();

cores.setAllowCredentials(true);//允许客户端携带认证信息

//springBoot 2.4.1版本之后,不可以用 * 号设置允许的Origin,如果不降低版本,则在跨域设置时使用setAllowedOriginPatterns方法

// cores.setAllowedOrigins(Collections.singletonList("*"));//允许所有域名可以跨域访问

cores.setAllowedOriginPatterns(Collections.singletonList("*"));

cores.setAllowedMethods(Arrays.asList("GET","POST","DELETE","PUT","UPDATE"));//允许哪些请求方式可以访问

cores.setAllowedHeaders(Collections.singletonList("*"));//允许服务端访问的客户端请求头

// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)

cores.addExposedHeader(jsonWebTokenUtil.getHeader());

// 注册跨域配置

// 也可以使用CorsConfiguration 类的 applyPermitDefaultValues()方法使用默认配置

source.registerCorsConfiguration("/**",cores.applyPermitDefaultValues());

return source;

}

}

全局配置

@EnableGlobalMethodSecurity(prePostEnabled = true)

@Configuration

public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired

UserService userService;

@Autowired

TokenFilter tokenFilter;

/**

* 配置 URL 访问权限

*/

@Override

protected void configure(HttpSecurity http) throws Exception {

//

http // 1.过滤请求

.authorizeRequests()

// 2.对于登录login 验证码captcha 允许访问

.antMatchers("/login").permitAll()

// 用户访问其它URL都必须认证后访问(登录后访问)

.anyRequest().authenticated()

.and()

// 3.关闭csrf

.csrf().disable()

// 4.基于token,所以不需要session

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

// 5.页面能不能以 frame、 iframe、 object 形式嵌套在其他站点中,用来避免点击劫持(clickjacking)攻击

.and().headers().frameOptions().disable();

// 异常处理

http.exceptionHandling()

// 未认证返回401

.authenticationEntryPoint((req, response, authException) -> {

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

PrintWriter out = response.getWriter();

out.write("尚未登录,请先登录");

out.flush();

out.close();

})

// 没有权限,返回403 json

.accessDeniedHandler((request, response, ex) -> {

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_FORBIDDEN);

PrintWriter out = response.getWriter();

Map map = new HashMap();

map.put("code", 403);

map.put("message", "权限不足");

out.write(JSONUtil.toJsonPrettyStr(map));

out.flush();

out.close();

});

// 配置登出

http.logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> {

// 删除用户token

response.setContentType("application/json;charset=utf-8");

response.setStatus(HttpServletResponse.SC_OK);

PrintWriter out = response.getWriter();

out.write("OK");

out.flush();

out.close();

});

// 添加JWT filter

http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

}

/**

* 配置用户及其对应的角色

*/

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(userService);

}

/**

* 适用于静态资源的防拦截,css、js、image 等文件

* 配置的url不会保护它们免受CSRF、XSS、Clickjacking等的影响。

* 相反,如果您想保护端点免受常见漏洞的侵害,请参阅configure(HttpSecurity)

*/

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/css/**", "/js/**");

}

@Bean

PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

/**

* 解决 无法直接注入 AuthenticationManager

*

*/

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception

{

return super.authenticationManagerBean();

}

}

参考:

https://blog.csdn.net/X_lsod/article/details/122914659

https://blog.csdn.net/godleaf/article/details/108318403

https://blog.csdn.net/qq_43437874/article/details/119543579


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java如何在Map中存放重复key(java map去重复)
下一篇:Java并发中的ABA问题学习与解决方案(java解决aba问题)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~