版本说明:
Spring Boot:2.2.1.RELEASE
Spring Security:
Spring Security 简介
Spring Security参考 | Spring Cloud 中文网
初级使用:默认用户认证
添加依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
只要加入依赖,项目中所有接口都会被自动保护起来。默认用户名为:user,密码在项目启动后自动生成。
1 Using generated security password: 6c920ced-f1c1-4604-96f7-f0ce4e46f5d4
也可以通过配置文件application.properties
指定用户名密码
1 2 spring.security.user.username =admin spring.security.user.username =123456
测试接口
1 2 3 4 5 6 7 @RestController public class HelloController { @GetMapping ("/hello" ) public String hello () { return "hello" ; } }
访问接口,将重定向到默认登陆页面,登陆成功之后,就会自动跳转到原来都接口。
另外:也可以使用 postman 来发送登陆请求(需要将登陆信息放在请求头中,避免重定向到登陆页面)
通过以上两种不同的登录方式,可以看出,Spring Security 支持 form 表单和 HttpBasic 两种不同的认证方式,对应到源码是.formLogin()
和.httpBasic()
:
1 2 3 4 5 6 7 8 9 10 11 12 public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer <WebSecurity > { protected void configure (HttpSecurity http) throws Exception { this .logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)." ); ((HttpSecurity) ((HttpSecurity) ((AuthorizedUrl) http.authorizeRequests() .anyRequest()) .authenticated() .and()) .formLogin() .and()) .httpBasic(); } }
中级使用:内存用户认证 创建 Security 配置类
SecurityConfig 1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); auth.inMemoryAuthentication() .withUser("admin" ).password(encoder.encode("admin" )).roles("admin" ) .and() .withUser("user" ).password(encoder.encode("user" )).roles("user" ); } }
高级使用:数据库用户认证 实现 UserDetailsService 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userService.selectUserByUsername(username); if (user == null ){ throw new UsernameNotFoundException("Username not exist!" ); } List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("user" )); return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities); } }
SecurityConfig.java 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailService userDetailService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } }
前后端分离后的认证
SpringBootSecurity学习(12)前后端分离版之简单登录
前后端分离后,后端不再需要提供登陆页面,只需要配置登陆地址即可。
开启授权认证 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); } }
配置处理登陆请求的 API 地址 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin().loginProcessingUrl("/user/login" ).usernameParameter("username" ).passwordParameter("password" ); } }
配置登陆成功处理器 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin().successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpServletResponse.getWriter().write("{\"code\": \"200\", \"msg\": \"登录成功\"}" ); } }); } }
默认登陆成功处理器源码截取
SimpleUrlAuthenticationSuccessHandler.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SimpleUrlAuthenticationSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler { public SimpleUrlAuthenticationSuccessHandler () { } public SimpleUrlAuthenticationSuccessHandler (String defaultTargetUrl) { this .setDefaultTargetUrl(defaultTargetUrl); } public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { this .handle(request, response, authentication); this .clearAuthenticationAttributes(request); } protected final void clearAuthenticationAttributes (HttpServletRequest request) { HttpSession session = request.getSession(false ); if (session != null ) { session.removeAttribute("SPRING_SECURITY_LAST_EXCEPTION" ); } } }
AbstractAuthenticationTargetUrlRequestHandler.java 1 2 3 4 5 6 7 8 9 10 11 public abstract class AbstractAuthenticationTargetUrlRequestHandler { protected void handle (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String targetUrl = this .determineTargetUrl(request, response, authentication); if (response.isCommitted()) { this .logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); } else { this .redirectStrategy.sendRedirect(request, response, targetUrl); } } }
配置登陆失败处理器 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin().failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpServletResponse.getWriter().write("{\"code\": \"500\", \"msg\": \"登录失败\"}" ); } }); } }
默认登陆失败处理器源码截取
SimpleUrlAuthenticationFailureHandler.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler { public void onAuthenticationFailure (HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (this .defaultFailureUrl == null ) { this .logger.debug("No failure URL set, sending 401 Unauthorized error" ); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { this .saveException(request, exception); if (this .forwardToDestination) { this .logger.debug("Forwarding to " + this .defaultFailureUrl); request.getRequestDispatcher(this .defaultFailureUrl).forward(request, response); } else { this .logger.debug("Redirecting to " + this .defaultFailureUrl); this .redirectStrategy.sendRedirect(request, response, this .defaultFailureUrl); } } } }
配置认证异常处理器 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpServletResponse.getWriter().write("{\"code\": \"401\", \"msg\": \"登陆已过期或未登陆\"}" ); } }); } }
默认认证异常处理器源码截取
LoginUrlAuthenticationEntryPoint.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint , InitializingBean { public void commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String redirectUrl = null ; if (this .useForward) { if (this .forceHttps && "http" .equals(request.getScheme())) { redirectUrl = this .buildHttpsRedirectUrlForRequest(request); } if (redirectUrl == null ) { String loginForm = this .determineUrlToUseForThisRequest(request, response, authException); if (logger.isDebugEnabled()) { logger.debug("Server side forward to: " + loginForm); } RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm); dispatcher.forward(request, response); return ; } } else { redirectUrl = this .buildRedirectUrlToLoginPage(request, response, authException); } this .redirectStrategy.sendRedirect(request, response, redirectUrl); } }
配置权限异常处理器 SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration @EnableGlobalMethodSecurity (securedEnabled = true , prePostEnabled = true , jsr250Enabled = true )public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() { @Override public void handle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpServletResponse.getWriter().write("{\"code\": \"402\", \"msg\": \"权限不足\"}" ); } }); } }
接口方法上使用注解@PreAuthorize
配置权限
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping ("hello" )public class HelloController { @Value ("${spring.profiles.active}" ) private String environment; @GetMapping ("env" ) @PreAuthorize ("hasRole('user')" ) public String getEnvironment () { return "The Environment is " + environment; } }
注意:如果配置了全局异常处理器,权限异常处理器就会失效,权限异常会先被全局异常处理器处理而到不了权限异常处理器。解决办法参考这里
默认权限异常处理器源码截取
AccessDeniedHandlerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class AccessDeniedHandlerImpl implements AccessDeniedHandler { public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (!response.isCommitted()) { if (this .errorPage != null ) { request.setAttribute("SPRING_SECURITY_403_EXCEPTION" , accessDeniedException); response.setStatus(HttpStatus.FORBIDDEN.value()); RequestDispatcher dispatcher = request.getRequestDispatcher(this .errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); } } } }
整合 JWT
SpringBootSecurity学习(13)前后端分离版之JWT
jjwt | GitHub
使用 session 和 cookie 来维护登录状态的扩展性不好,使用 JWT 的 token 验证方式则可以很好的适应分布式场景的需求。
导入 jjwt 包 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-api</artifactId > <version > 0.10.7</version > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-impl</artifactId > <version > 0.10.7</version > <scope > runtime</scope > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-jackson</artifactId > <version > 0.10.7</version > <scope > runtime</scope > </dependency >
关闭 Session SecurityConfigurerImpl.java 1 2 3 4 5 6 7 8 9 @Configuration @EnableGlobalMethodSecurity (securedEnabled = true , prePostEnabled = true , jsr250Enabled = true )public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
源码截取
SessionCreationPolicy.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public enum SessionCreationPolicy { ALWAYS, NEVER, IF_REQUIRED, STATELESS }
JWT 工具类 JWTUtil.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class JWTUtil { private static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); public final static String HEADER_TOKEN_NAME = "Authentication-Token" ; public static String genericToken (SysUser user) { user.setPassword(null ); return Jwts.builder() .setIssuer("gzhennaxia" ) .setIssuedAt(new Date()) .claim("user" , JSON.toJSONString(user)) .signWith(KEY) .compact(); } public static SysUser parseToken2User (String token) { Jws<Claims> jws = Jwts.parser() .requireIssuer("gzhennaxia" ) .setSigningKey(KEY) .parseClaimsJws(token); return JSON.parseObject(jws.getBody().get("user" ).toString(), SysUser.class ) ; } }
修改登录成功处理器 AuthenticationSuccessHandlerImpl.java 1 2 3 4 5 6 7 8 9 10 @Component public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { User user = (User) authentication.getPrincipal(); httpServletResponse.setHeader(JWTUtil.HEADER_TOKEN_NAME, JWTUtil.genericToken(SysUser.builder().username(user.getUsername()).build())); httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); httpServletResponse.getWriter().write(ReturnVo.errorInJson(CodeEnum.SUCCESS)); } }
配置 JWT 过滤器 JWTAuthenticationFilter.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Component public class JWTAuthenticationFilter extends GenericFilterBean { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader(JWTUtil.HEADER_TOKEN_NAME); if (!StringUtils.isEmpty(token)) { try { SysUser sysUser = JWTUtil.parseToken2User(token); SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(sysUser, null , sysUser.getAuthorities())); } catch (Exception e) { e.printStackTrace(); servletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); if (e instanceof MissingClaimException) { servletResponse.getWriter().write(ReturnVo.errorInJson(CodeEnum.MISSING_CLAIM)); } else if (e instanceof IncorrectClaimException) { servletResponse.getWriter().write(ReturnVo.errorInJson(CodeEnum.INCORRECT_CLAIM)); } else { servletResponse.getWriter().write(ReturnVo.errorInJson(CodeEnum.UNKNOWN_PARSE_ERROR)); } return ; } } filterChain.doFilter(servletRequest, servletResponse); } }
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class SecurityConfigurerImpl extends WebSecurityConfigurerAdapter { @Autowired private JWTAuthenticationFilter jwtAuthenticationFilter; @Override protected void configure (HttpSecurity http) throws Exception { http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class ) ; } }
使用 JSON 格式数据登陆
Spring Security 登录使用 JSON 格式数据
Spring Security配置JSON登录
Spring Security 默认支持的登陆方式为 formLogin,即 Content-Type 为application/x-www-form-urlencoded
,如果想要使用 JSON 数据登陆,则需要自定义提取用户名密码的过滤器来替换默认的UsernamePasswordAuthenticationFilter
。
CustomerAuthenticationFilter.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class CustomerAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String contentType = request.getContentType(); if (contentType.equals(MediaType.APPLICATION_JSON_VALUE) || contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) { UsernamePasswordAuthenticationToken authRequest; try (InputStream in = request.getInputStream()) { SysUser user = JSON.parseObject(in, SysUser.class ) ; authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken("" , "" ); } return this .getAuthenticationManager().authenticate(authRequest); } return super .attemptAuthentication(request, response); } }
SecurityConfig.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration @EnableGlobalMethodSecurity (securedEnabled = true , prePostEnabled = true , jsr250Enabled = true )public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.addFilterAt(this .customerAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class ) ; } @Bean CustomerAuthenticationFilter customerAuthenticationFilter () throws Exception { CustomerAuthenticationFilter filter = new CustomerAuthenticationFilter(); filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login" , "POST" )); filter.setAuthenticationSuccessHandler(authenticationSuccessHandler); filter.setAuthenticationFailureHandler(authenticationFailureHandler); filter.setAuthenticationManager(this .authenticationManagerBean()); return filter; } }
注意:
处理登陆请求的 URL 地址不能通过之前的方式(.formLogin().loginProcessingUrl("/user/login")
)来配置,需要在自定义过滤器里配置,因为以前的方式配置的是默认的过滤器的,自定义过滤器会替换掉默认的,所以用以前的方式配置的 loginProcessingUrl 就失效了。
自定义的过滤器之所以要在 security 配置类中初始化(使用@Bean
的方式而不是直接在类上使用@Component
),是因为需要使用WebSecurityConfigurerAdapter
的authenticationManagerBean()
方法来调用默认的认证管理器,否则需要自定义自己的认证管理器。
自定义认证管理器 为了把提取用户名密码的过滤器从配置类中抽离出来,就需要使用自定义的认证管理器来替换默认的认证管理器(WebSecurityConfigurerAdapter.AuthenticationManagerDelegator
)。由于默认的认证管理器是不可继承的(由final
修饰),所以通过实现AuthenticationManager
接口来自定义,而认证管理器又是通过认证处理器AuthenticationManager
提供的认证方法authenticate()
最终对用户进行认证的,所以在自定义认证管理器的时候还需要提供认证处理器,可以在认证管理器的构造方法中直接初始化认证处理器,这里使用默认的持久层认证处理器为DaoAuthenticationProvider
。
CustomerAuthenticationManagerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component public class CustomerAuthenticationManagerImpl implements AuthenticationManager { private final AuthenticationProvider provider; public CustomerAuthenticationManagerImpl (SecurityUserDetailService userDetailsService) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(new BCryptPasswordEncoder()); this .provider = provider; } @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { Authentication authenticate = this .provider.authenticate(authentication); if (Objects.nonNull(authenticate)) { return authenticate; } throw new ProviderNotFoundException("Authentication failed!" ); } }
SecurityConfig.java 1 2 3 4 5 6 7 8 9 10 11 12 @Configuration @EnableGlobalMethodSecurity (securedEnabled = true , prePostEnabled = true , jsr250Enabled = true )public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomerAuthenticationFilter authenticationFilter; @Override protected void configure (HttpSecurity http) throws Exception { http.addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class ) ; } }
注意:和自定义用于提取用户名密码的过滤器道理一样,因为是替换默认的认证处理器,所以 UserDetailsService、PasswordEncoder 等配置需要在初始化认证处理器的地方进行配置,而不能用以前配置默认认证处理器的方式。
自定义认证处理器 如果不想在自定义的认证管理器中使用默认的认证处理器,也可以自定义认证处理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component public class CustomerAuthenticationProviderImpl extends DaoAuthenticationProvider { @Autowired UserService userService; private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { String username = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); SysUser user = userService.selectUserByUsername(username); user.setRoleList(Collections.singletonList(new SysRole("user" ))); if (PASSWORD_ENCODER.matches(password, user.getPassword())) { return new UsernamePasswordAuthenticationToken(username, password, user.getRoleList()); } else { throw new BadCredentialsException(this .messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } } @Override protected void doAfterPropertiesSet () { } }
CustomerAuthenticationManagerImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component public class CustomerAuthenticationManagerImpl implements AuthenticationManager { private final AuthenticationProvider provider; public CustomerAuthenticationManagerImpl (AuthenticationProvider provider) { this .provider = provider; } @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { Authentication authenticate = this .provider.authenticate(authentication); if (Objects.nonNull(authenticate)) { return authenticate; } throw new ProviderNotFoundException("Authentication failed!" ); } }