一、简介
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")
@ResponseBody
public 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 {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource ;
/**
//提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager =
new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("JunSouth").password("123").authorities("admin").build());
return inMemoryUserDetailsManager;
}
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();
obj.setDataSource(dataSource);
//obj.setCreateTableOnStartup(false);
//启动创建表persistent_logs表,存token,username时会用到
return obj;
}
@Bean
public PasswordEncoder passwordEncoder(){
//return new BCryptPasswordEncoder();
//密码编码器:不加密
return NoOpPasswordEncoder.getInstance();
}
//授权规则配置
@Override
protected 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 {
@Autoware
private LoginMapper loginMapper;
/**
* 加载数据库中的认证的用户的信息:用户名、密码、用户的权限列表
* 通过username查询用户的信息,(密码,权限列表等)封装成 UserDetails 返回,交给 security 。
*/
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Override
public 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 {
@Test
public void testPassword(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String enPass = bCryptPasswordEncoder.encode("123");
System.out.println(enPass);
System.out.println(bCryptPasswordEncoder.matches("123", enPass));
}
}