Spring Security
-
开发Web应用,对页面的安全控制通常是必须的。比如:对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样,可以通过Aop、拦截器实现,也可以通过框架实现,例如:Apache Shiro、Spring Security 。Spring Security 认证流程
- UsernamePasswordAuthenticationFilter 实现获得用户名和密码的操作,并且只允许post请求方法
SecurityContextPersistenceFilter extends GenericFilterBean{ } AbstractAuthenticationProcessingFilter extends GenericFilterBean{ } UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //return super.attemptAuthentication(request, response); if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod()); } String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, token); // 通过调用 getAuthenticationManager() 来获取 AuthenticationManager 对象,通过调用它的 authenticate 方法来查找支持该 token 认证方式的 provider,然后调用该 provider 的 authenticate 方法进行认证 return this.getAuthenticationManager().authenticate(authRequest); } }
- AuthenticationManager 接口有一个 authenticate 方法,通过该方法调用 AuthenticationProvider 的authenticate方法
public class ProviderManager implements AuthenticationManager { // 维护一个AuthenticationProvider列表 private List<AuthenticationProvider> providers = Collections.emptyList(); public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); // 获得认证方式的 provider Iterator var6 = this.getProviders().iterator(); //依次来认证 while(var6.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var6.next(); if (provider.supports(toTest)) { try { // 如果有Authentication信息,则直接返回 result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (AccountStatusException var11) { this.prepareException(var11, authentication); throw var11; } catch (InternalAuthenticationServiceException var12) { this.prepareException(var12, authentication); throw var12; } catch (AuthenticationException var13) { lastException = var13; } } } } }
- DaoAuthenticationProvider 主要完成用户授权校验,authenticate方法会调用 additionalAuthenticationChecks方法,来对请求的用户名和密码进行校验。如果校验不通过,则返回 Bad credentials
AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider{ } DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{ // retrieveUser方法,它返回UserDetails类 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //调用 UserDetailsService 对象的 loadUserByUsername这个方法; loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException var6) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null); } throw var6; } catch (Exception var7) { throw new InternalAuthenticationServiceException(var7.getMessage(), var7); } if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } // additionalAuthenticationChecks方法,拿到通过用户姓名获得的该用户的信息(密码等)和用户输入的密码加密后对比,如果不正确就会报错Bad credentials的错误 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { //此方法在你的配置文件中去配置实现的 也是spring security加密的关键 ------划重点 salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { this.logger.debug( "Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } } }
- DaoAuthenticationProvider 类中的 retrieveUser方法,是用来返回UserDetails类的,UserDetails是Spring对用户身份信息封装的一个接口。使用 UserDetailsService 类可以从特定的地方(通常是数据库)加载用户信息。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
-
Spring Boot 集成 Spring Security
-
创建一个 demo 项目 , 可以正常访问:http://localhost:8080/hello
-
修改 pom 文件, 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
- 添加依赖后,整个应用就有了默认的安全机制,当再次打开URL,你将看到一个alert表单对话框,需要输入用户名和密码
- security默认的用户名是user, 默认密码是应用启动的时候,通过UUID算法随机生成的。
-
通过 application.propertiesm配置你可以修改默认用户名和密码
spring.security.user.name=admin spring.security.user.password=123456
-
使用内存用户名密码认证,自定义一个配置类 SecurityConfig , HttpSecurity 方法介绍
@Configuration @EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 在任何应用中,并不是所有请求都需要同等程度地保护起来。有些请求需要认证,有些则不需要。 // 对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法 @Override protected void configure(HttpSecurity http) throws Exception { // 默认配置, 父类中的实现 : super.configure(http); http.authorizeRequests() .anyRequest().authenticated() // 所有请求需要身份认证 .and().formLogin() .and().httpBasic(); // http.authorizeRequests() // 对请求进行认证 .antMatchers("/", "/hello").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") // 角色检查 .antMatchers("/user/**").hasRole("USER") // 角色检查 .antMatchers("/**").hasAnyRole("ADMIN", "USER") .and().formLogin() .and().logout().logoutSuccessUrl("/login").permitAll() .and().csrf().disable(); // 关闭csrf验证 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 使用内存用户名密码认证 auth.inMemoryAuthentication() .withUser("user").password(passwordEncoder().encode("1234")).roles("USER") .and().withUser("admin").password(passwordEncoder().encode("admin")) .roles("ADMIN", "USER"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
-
用数据库存储用户和角色,实现安全认证
-
数据库层设计:新建三张表User,Role,UserRole
@Entity class User { private Integer id; private String userName; private String password; } @Entity class Role { private Integer id; private String role; } @Entity class UserRole { private Integer id; private Integer userId; private Integer roleId; }
-
配置类 SecurityConfig
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override @Bean public UserDetailsService userDetailsService() { //覆盖写userDetailsService方法 return new CustomUserDetailService(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 对请求进行认证 .antMatchers("/", "/hello").permitAll() .antMatchers("/admin/**").hasAuthority("ADMIN") // 授权检查 .antMatchers("/user/**").hasAuthority("USER") // 授权检查 .antMatchers("/**").hasAnyAuthority("ADMIN","USER") .and().formLogin() .and().logout().logoutSuccessUrl("/login").permitAll() .and().csrf().disable(); // 关闭csrf验证 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); //从数据库获取用户和角色 } }
-
实现 UserDetailsService 子类
public class CustomUserDetailService implements UserDetailsService { @Autowired UserDao userDao; @Autowired RoleDao roleDao; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userDao.getUserByUsername(username); ArrayList<String> userRoles = roleDao.listByUserId(user.getId()); List<SimpleGrantedAuthority> list = new ArrayList<SimpleGrantedAuthority>(); for (String roleName : userRoles) { list.add(new SimpleGrantedAuthority(roleName)); } return new org.springframework.security.core.userdetails.User( user.getUserName(), user.getPassword(), list); } }
-
自定义登录页面 Spring Security 5 Login Form Example
- ISSUE: 遇到的问题是 login.html 页面 的 用户名和密码的控件,一定要带上 name=“username”,因为这是参数
- Spring Boot + Spring Security + Thymeleaf example
- ISSUE : post 方法没有权限 : 403错误。 在页面添加控件:
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
-
-
-
修改pom 文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
-
添加配置类
@Configuration @EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 设置 HTTP 验证规则 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 对请求进行认证 .antMatchers("/hello").permitAll() // 不需要身份认证 .antMatchers("/admin/**").hasRole("ADMIN") // 角色检查 .antMatchers("/**").hasAnyRole("ADMIN", "USER") .antMatchers("/hello").hasAuthority("AUTH_WRITE") // 权限检查 .and().formLogin() .and().logout().logoutSuccessUrl("/login").permitAll() .and().csrf().disable(); // 关闭csrf验证 // 在UsernamePasswordAuthenticationFilter 之前添加一个过滤器(让所有 /login 的请求都经过 JWTLoginFilter 过滤器,进行登录校验,验证成功后,生成JWT 返回) .and().addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) // 在UsernamePasswordAuthenticationFilter 之前添加一个过滤器(让所有请求都经过JWTAuthenticationFilter 过滤器,从JWT中取出username和authentication,并生成Token,验证Token是否合法) .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 使用自定义的身份验证组件,代替默认的 DaoAuthenticationProvider auth.authenticationProvider(new CustomAuthenticationProvider()); } }
-
-
Spring Boot Security 整合 OAuth2 设计安全API接口服务
-
oauth2 知识介绍
- oauth2授权主要由两部分组成:认证服务(Authorization server)和 资源服务(Resource server)
- oauth2 主要支持4种模式4种 grant_type
- authorization_code :授权码模式,即先登录获取code,再获取token
- client_credentials :客户端模式,用户向客户端注册,然后客户端以自己的名义向’服务端’获取资源,无用户
- password : 密码模式,将用户名 密码传过去,直接获取token
- refresh_token :刷新access_token
-
建表:Spring OAuth2 己经设计好了数据库的表,且不可变。表及字段说明参照:Oauth2数据库表说明
-
pom 文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.8.RELEASE</version> </dependency>
-
新增 AuthorizationServerConfiguration 配置类
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // 注入authenticationManager 来支持 password grant type @Autowired private AuthenticationManager authenticationManager; // 配置内存存储 token @Bean public TokenStore tokenStore() { return new InMemoryTokenStore(); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // 配置oauth2服务跨域 CorsConfigurationSource source = new CorsConfigurationSource() { @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedOrigin(request.getHeader( HttpHeaders.ORIGIN)); corsConfiguration.addAllowedMethod("*"); corsConfiguration.setAllowCredentials(true); corsConfiguration.setMaxAge(3600L); return corsConfiguration; } }; // 意思是 /oauth/token 路径 不需要授权就可以访问 security.tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients() .addTokenEndpointAuthenticationFilter(new CorsFilter(source)); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 为了测试方便,我们先插入一条客户端信息 clients.inMemory().withClient("dev").secret("dev").scopes("app") .authorizedGrantTypes("client_credentials","authorization_code", "password","refresh_token") .redirectUris("http://www.baidu.com") .refreshTokenValiditySeconds(360).accessTokenValiditySeconds(360) .autoApprove(false); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) // 配置内存存储 token , 使用authenticationManager 来支持 password grant type .authenticationManager(authenticationManager); } }
-
新增 ResourceServerConfig
// 配置 OAuth2 管理的资源,和 WebSecurityConfig 授权配置一样 @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/hi") .and() .authorizeRequests() .antMatchers("/hi").authenticated(); } }
-
修改 WebSecurityConfig , 支持 password 模式
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/hello").permitAll() .antMatchers("/**").authenticated() // .anyRequest().authenticated() .and().formLogin() .and().httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user") .password(passwordEncoder().encode("1234")) .roles("USER"); } // 支持 OAuth2 的密码编码方式, 因为OAuth2 不支持 BCrypt @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; } // 需要配置这个支持password模式 , support password grant type @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
-
测试: 密码授权模式、刷新 token 模式 、客户端授权模式
#密码授权模式 curl -X POST -d "username=user&password=1234&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token {"error":"unsupported_grant_type","error_description":"Unsupported grant type"} {"access_token":"b541cb47-aab8-48b5-b845-dfe002e4dbe3","token_type":"bearer","refresh_token":"8cfad00a-3ce0-4925-bb8c-bf52725d1fec","expires_in":359,"scope":"app"} # 刷新 token 模式 curl -X POST -d "grant_type=refresh_token&refresh_token=8cfad00a-3ce0-4925-bb8c-bf52725d1fec&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token {"error":"server_error","error_description":"Internal Server Error"} # 意思是 UserDetailsService is required #客户端授权模式 curl -X POST -d "grant_type=client_credentials&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token {"access_token":"598ab278-b5f0-484b-bdc6-ff01199c3631","token_type":"bearer","expires_in":359,"scope":"app"} # 访问 URL curl http://localhost:8080/hi\?name\=zhangsan\&access_token\=b541cb47-aab8-48b5-b845-dfe002e4dbe3
-
测试授权码模式
http://localhost:8080/oauth/authorize?response_type=code&client_id=dev&redirect_uri=http://www.baidu.com
-
其他
- SpringBoot + Spring Security OAuth2基本使用 运行不起来,报错:Field configurers in org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration required a bean of type ‘java.util.List’ that could not be found
-