一、简介
1.1、Spring Security 相关概念
1.过滤器链(Filter Chain)
基于Servlet过滤器(Filter)处理和拦截请求,进行身份验证、授权等安全操作。过滤器链按顺序执行,每个过滤器负责一个具体的安全功能。
2.SecurityInterceptor(安全拦截器)
根据配置的安全规则拦截请求,进行访问控制和权限验证。
3.Authentication(认证对象)
封装用户的认证信息(账户状态、用户名、密码、权限等)
Authentication常用实现类:
UsernamePasswordAuthenticationToken:用户名密码登录的 Token
AnonymousAuthenticationToken:针对匿名用户的 Token
RememberMeAuthenticationToken:记住我功能的的 Token
4.AuthenticationManager (用户认证的管理类)
所有的认证请求都会封装成一个Token 给 AuthenticationManager,AuthenticationManager 调用 AuthenticationProvider.authenticate() 认证,返回包含认证信息的 Authentication 对象。
5.AuthenticationProvider(认证的具体实现类)
一个 provider 是一种认证方式实现,主流的认证方式都已经提供了默认实现,如 DAO、LDAP、CAS、OAuth2等。
6.UserDetailService(用户详细信息服务)
通过 UserDetailService 拿到数据库(或内存)中的认证信息然后和客户端提交的认证信息做校验。
7.访问决策管理器(AccessDecisionManager)
在授权过程中进行访问决策。根据用户的认证信息、请求的URL和配置的权限规则,判断用户是否有权访问资源。
8.SecurityContext(安全上下文)
认证通过后,会为这用户生成一个唯一的 SecurityContext(ThreadLocal存储),包认证信息 Authentication。
通过 SecurityContext 可获取到用户的标识 Principle 和授权信息 GrantedAuthrity。
系统任何地方只要通过 SecurityHolder.getSecruityContext() 可获取到 SecurityContext。
9.注解和表达式支持
用在代码中声明和管理安全规则。如@Secured注解可以标记在Controller或方法上,限制权限用户才能访问。
1.2、核心的过滤器链
1.SecurityContextPersistenceFilter
Filter的入口和出口,将 SecurityContext (登录后的信息)对象持久到Session,同时把 SecurityContext 设置给 SecurityContextHolder 获取用户认证授权信息。
2.UsernamePasswordAuthenticationFilter
默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息封装成 UsernamePasswordAuthenticationToken,然后调 AuthenticationManager 进行认证。
3.BasicAuthenticationFilter
基本认证,支持 httpBasic 认证方式的Filter。
4.RememberAuthenticationFilter
记住我功能实现的 Filter。
5.AnonymousAuthenticationFilter
处理匿名访问的资源,如果用户未登录,会创建匿名的Token(AnonymousAuthenticationToken),通过 SecurityContextHodler 设置到 SecurityContext 中。
6.ExceptionTranslationFilter
捕获 FilterChain 所有的异常,但只处理 AuthenticationException、AccessDeniedException 异常,其他的异常会继续抛出。
7.FilterSecurityInterceptor
做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用 AccessDecisionManager.decide 方法对用户授权。
1.3、Spring Security 使用场景
1.用户登录和认证
可处理用户的身份验证。如表单登录、基本认证、OAuth等。
2.授权和权限管理
可定义安全规则和访问控制,可用注解、表达式或配置文件来声明和管理权限,确保用户只能访问其有权访问的资源。
3.防止跨站点请求伪造(CSRF)
可生成和验证CSRF令牌,防止Web应用程序受到CSRF攻击。可在表单中自动添加CSRF令牌,并验证提交请求中的令牌值。
4.方法级安全性
允许在方法级别对方法进行安全性配置。可用注解或表达式来定义哪些用户有权调用特定方法。
5.记住我功能
允许用户在下次访问时保持登录状态,不要重新输入用户名和密码。
6.单点登录(SSO)
可与其他身份验证和授权提供程序集成,实现单点登录。
7.安全事件和审计日志
可记录安全事件和用户操作,以便进行审计和故障排查。
二、SpringBoot 中基本使用
2.1、引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId>
</dependency>
2.2、外围的配置
2.2.1、登录页面static/login.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login"><div class="checkbox"><label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label></div><div>用户名:<input type="text" name="username"></div><div>密码:<input type="password" name="password"></div><div><button type="submit">立即登陆</button></div>
</form>
</body>
</html>
2.2.2、启动类、访问接口
@SpringBootApplication
public class ApplicationConfig {public static void main(String[] args) {SpringApplication.run(ApplicationConfig.class);}
}@Controller
public class AuthController {//登录成功后重定向地址@RequestMapping("/loginSuccess")@ResponseBodypublic String loginSuccess(){return "登录成功";}
}
2.2.3、DDL、实体类、Dao层接口
SET FOREIGN_KEY_CHECKS=0;-- ------------------------------ Table structure for t_permission 权限-- ----------------------------DROP TABLE IF EXISTS `t_permission`;CREATE TABLE `t_permission` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`resource` varchar(255) NOT NULL,`state` int(11) DEFAULT NULL,`menu_id` bigint(20) DEFAULT NULL,`expression` varchar(255) NOT NULL,PRIMARY KEY (`id`),KEY `menu_id` (`menu_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for t_role 角色-- ----------------------------DROP TABLE IF EXISTS `t_role`;CREATE TABLE `t_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`sn` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for t_role_permission 角色和权限关系-- ----------------------------DROP TABLE IF EXISTS `t_role_permission`;CREATE TABLE `t_role_permission` (`role_id` bigint(20) NOT NULL,`permission_id` bigint(20) NOT NULL,PRIMARY KEY (`role_id`,`permission_id`),KEY `permission_id` (`permission_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for t_login 用户登录表-- ----------------------------DROP TABLE IF EXISTS `t_login`;CREATE TABLE `t_login` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(255) DEFAULT NULL COMMENT '员工用户名',`password` varchar(255) DEFAULT NULL COMMENT '密码',PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for t_login_role 用户角色关系表-- ----------------------------DROP TABLE IF EXISTS `t_login_role`;CREATE TABLE `t_login_role` (`login_id` bigint(20) NOT NULL,`role_id` bigint(20) NOT NULL,PRIMARY KEY (`login_id`,`role_id`),KEY `role_id` (`role_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ------------------------------ Table structure for persistent_logins 用户登录记住表-- ----------------------------CREATE TABLE `persistent_logins` (`username` varchar(64) NOT NULL DEFAULT '',`series` varchar(64) NOT NULL,`token` varchar(64) NOT NULL,`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`series`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
public class Login {private Long id;private String username;private String password;
}public class Permission {private Long id;private String name;private String sn;private String resource;
}import cn.itsource.security.domain.Login;
import cn.itsource.security.domain.Permission;
import java.util.List;public interface LoginMapper {Login selectByUsername(String username);
}import cn.itsource.security.domain.Permission;
import java.util.List;public interface PermissionMapper {List<Permission> selectPermissionsByUserId(Long userId);List<Permission> selectAll();
}
2.2.4、数据库连接、数据插入
略 . . . . . .
2.3、配置类 WebSecurityConfigurerAdapter
配置类 WebSecurityConfigurerAdapter
创建UserDetailService的Bean,用来加载用户认证信息。
配置编码器,通过该编码器对密码进行加密匹配。
授权规则配置,哪些资源需要什么权限。@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate DataSource dataSource ;/**//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("JunSouth").password("123").authorities("admin").build());return inMemoryUserDetailsManager;}*/@Beanpublic PersistentTokenRepository persistentTokenRepository(){JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();obj.setDataSource(dataSource);//obj.setCreateTableOnStartup(false); //启动创建表persistent_logs表,存token,username时会用到return obj;}@Beanpublic PasswordEncoder passwordEncoder(){//return new BCryptPasswordEncoder();//密码编码器:不加密return NoOpPasswordEncoder.getInstance();}//授权规则配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/login").permitAll() //登录路径放行.antMatchers("/login.html").permitAll() //对登录页面跳转路径放行.anyRequest().authenticated() //其他路径都要拦截.and().formLogin() //允许表单登录, 设置登陆页.successForwardUrl("/loginSuccess") // 设置登陆成功页.loginPage("/login.html") //登录页面跳转地址.loginProcessingUrl("/login") //登录处理地址(必须).and().logout().logoutUrl("/mylogout").permitAll() //制定义登出路径.logoutSuccessHandler(new MyLogoutHandler()) //登出后处理器-可以做一些额外的事情.invalidateHttpSession(true); //登出后session无效http.rememberMe().tokenRepository(persistentTokenRepository()) //持久.tokenValiditySeconds(3600) //过期时间.userDetailsService(userDetailsService); //用来加载用户认证信息的}
}
2.4、 定义 UserDetailsService
/*** 提供给 security 的用户信息的 service,要复写 loadUserByUsername 方法返回数据库中的用户信息*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowareprivate LoginMapper loginMapper;/*** 加载数据库中的认证的用户的信息:用户名、密码、用户的权限列表* 通过username查询用户的信息,(密码,权限列表等)封装成 UserDetails 返回,交给 security 。*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Login userFromMysql = loginMapper.selectByUsername(username);if(loginFromMysql == null){throw new UsernameNotFoundException("无效的用户名");}//前台用户List<GrantedAuthority> permissions = new ArrayList<>();List<Permission> permissionSnList = systemManageClient.listByUserId(loginFromMysql.getId());permissionSnList.forEach(permission->{System.out.println("用户:"+username+" :加载权限 :"+permission.getSn());permissions.add(new SimpleGrantedAuthority(permission.getSn()));});//密码是基于BCryptPasswordEncoder加密的密文,User是security内部的对象,UserDetails的实现类 ,//用来封装用户的基本信息(用户名,密码,权限列表),四个 true 分别是账户启用、账户过期、密码过期、账户锁定return new User(username,loginFromMysql.getPassword(),true,true,true,true,permissions);}
}
2.5、认证结果处理
/*** 认证——成功处理*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");Map map = new HashMap<>();map.put("success",true);map.put("message","认证成功");map.put("data",authentication);response.getWriter().print(JSON.toJSONString(map));response.getWriter().flush();response.getWriter().close();}
}/*** 认证——失败处理*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");Map map = new HashMap<>();map.put("success",false);map.put("message","认证失败");response.setStatus(HttpStatus.UNAUTHORIZED.value());response.getWriter().print(JSON.toJSONString(map));response.getWriter().flush();response.getWriter().close();}
}
2.6、授权结果处理
/*** 授权失败——定义认证检查失败处理*/
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {String result = JSON.toJSONString(AjaxResult.me().setSuccess(false).setMessage("无访问权限"));response.setContentType("text/html;charset=utf-8");PrintWriter writer = response.getWriter();writer.print(result);writer.flush();writer.close();}
}/*** 授权失败——定义匿名用户访问无权处理*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {e.printStackTrace();httpServletResponse.setContentType("application/json;charset=utf-8");Map<String,Object> result = new HashMap<>();result.put("success",false);result.put("message","登录失败,用户名或密码错误["+e.getMessage()+"]");httpServletResponse.getWriter().print(JSONUtils.toJSONString(result));}
}
2.7、登出处理
/*** 登出处理器*/
public class MyLogoutHandler implements LogoutHandler {@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {try {//登出成功,响应结果给客户端,通常是一个JSON数据response.getWriter().println("logout success");} catch (IOException e) {e.printStackTrace();}}
}
2.8、操作—注册、登录
http://localhost:8080/login,
进入登录页面,输入账号:JunSouth 密码 123 完成登录
public class PasswordTest {@Testpublic void testPassword(){BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();String enPass = bCryptPasswordEncoder.encode("123");System.out.println(enPass);System.out.println(bCryptPasswordEncoder.matches("123", enPass));}
}