Spring Security 框架基础
应用入门
初始化 spring boot 项目
添加 pom.xml 文件
<!--添加Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@RestController
@RequestMapping("/demo")
public class DemoController {
@RequestMapping("/handle01")
public String handle01() {
return "你好 spring security";
}
}
访问 127.0.0.1:8080/demo/handle01 会进入表单页面。
默认 username=user;password 打印在控制台。
认证
httpBasic 认证
传输的用户名和密码使用 base64模式进行加密;是可逆的;不安全。
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
}
}
表单认证
默认的认证方式。UsernamePasswordAuthenticationFilter。默认的登录url : /login POST;参数名:username password 。
开启表单认证:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests()
.antMatchers("/demo/login", "/code/**").permitAll() // 不需要验证的接口
.anyRequest().authenticated();
}
}
解决静态资源拦截问题
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
// 排除静态资源被拦截
web.ignoring().antMatchers("/images/**");
}
}
基于数据库实现认证功能
操作实体采用 spring-data-jpa。
基本语法可参考:https://blog.youkuaiyun.com/qq_43439920/article/details/123165903?spm=1001.2014.3001.5501
开启后,会去数据库验证用户是否合法。控制台不会再打印密码。
@Service
public class PayingUserService implements UserDetailsService {
@Autowired
private PayingUserDao payingUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 基于数据库进行验证
PayingUser payingUser = new PayingUser();
payingUser.setUsername(username);
Example<PayingUser> payingUserExample = Example.of(payingUser);
Optional<PayingUser> payingUserOptional = payingUserDao.findOne(payingUserExample);
if (payingUserOptional.isEmpty()) {
// 用户名没有找到
System.out.println(username + "没有找到...");
throw new UsernameNotFoundException(username);
}
PayingUser user = payingUserOptional.get();
// 权限集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
// {noop}:密码的加密方式; noop => 不加密
return new User(user .getUsername(), "{noop}" + user .getPassword(), authorities);
}
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private PayingUserService payingUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义用户认证
auth.userDetailsService(payingUserService);
}
}
用户密码加密认证
// 推荐加密使用:bcrypt 强哈希方法 每次加密的结果都不一样 所以更安全
return new User(user .getUsername(), "{bcrypt}" + user .getPassword(), authorities);
@RequestMapping(value = "/save")
public PayingUser save(String username, String password) {
// bcrypt 加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encodePassword = bCryptPasswordEncoder.encode(password);
// 将加密后的数据存入数据库
PayingUser payingUser = new PayingUser();
payingUser.setUsername(username);
payingUser.setPassword(encodePassword);
payingUser.setStatus(1);
return payingUserDao.save(payingUser);
}
获取当前用户的三种方式
// 获取当前用户的三种方式
@RequestMapping("/getCurrentUser1")
public UserDetails getCurrentUser1() {
return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@RequestMapping("/getCurrentUser2")
public UserDetails getCurrentUser2(Authentication authentication) {
return (UserDetails) authentication.getPrincipal();
}
@RequestMapping("/getCurrentUser3")
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
remember-me
方便用户下一次登录的时候,不用再次输入用户名和密码登录。
简单Token
// 记住我功能 默认token失效时间是两周 单位 S
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me")
持久化Token
@Autowired
private DataSource dataSource;
@Bean
// 持久化 Token 支持
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 自动创建记录 token 的表 下次启动需要注释掉
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
.and().rememberMe().tokenValiditySeconds(60).rememberMeParameter("remember-me").tokenRepository(persistentTokenRepository())
# 默认创建的 token 表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`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;
判断请求是否来自 remember-me
// 判断当前操作是否来自 remember-me
@RequestMapping("/{id}")
public PayingUser getById(@PathVariable Integer id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
System.out.println("认证信息来源 remember-me,请重新登录");
throw new RememberMeAuthenticationException("认证信息来源 remember-me,请重新登录");
}
return payingUserDao.findById(id).get();
}
自定义登录成功&失败&退出处理逻辑
@Service
public class AuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败的后续处理...");
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功的后续处理...");
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出登录的后续处理...");
}
}
@Autowired
private AuthenticationService authenticationService;
.successHandler(authenticationService)
.failureHandler(authenticationService)
图形验证码
生成图形验证码的接口需要不被拦截。
/**
* 处理生成验证码的请求
*/
@RestController
@RequestMapping("/code")
public class ValidateCodeController {
public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
public final static int expireIn = 600; // 验证码有效时间 60s
@Autowired
public StringRedisTemplate stringRedisTemplate;
@RequestMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取访问IP
String remoteAddr = request.getRemoteAddr();
//生成验证码对象
ImageCode imageCode = createImageCode();
//生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr).set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
//通过IO流将生成的图片输出到登录页面上
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
/*
用于生成验证码对象
*/
private ImageCode createImageCode() {
int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
//创建一个带缓冲区图像对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获得在图像上绘图的Graphics对象
Graphics g = image.getGraphics();
Random random = new Random();
//设置颜色、并随机绘制直线
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//生成随机数 并绘制
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand.toString());
}
/*
获取随机演示
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
session 管理
会话超时
application.yml
server:
servlet:
session:
# 设置 session 的过期时间;默认 30 min;spring boot 最低 60 S
timeout: 60
并发控制
// session 过期后的处理
http.sessionManagement()
// 无效后跳转页面
.invalidSessionUrl("/demo/login")
// 最大会话数量
.maximumSessions(1)
// 当达到最大会话数量的时候不允许登录
.maxSessionsPreventsLogin(true);
集群session
一个服务会至少部署在两台服务器。如果第一次用户访问的是服务器1,第二次访问的是服务器2,用户会多次输入密码。
解决上述情况,可以将 session 存在 redis 中。
pom.xml
<!-- 基于 redis 实现 session 共享-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
application.yml
spring:
session:
# session 的存储方式
store-type: redis
跨域支持
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 设置允许跨域的站点
corsConfiguration.addAllowedOrigin("*");
// 设置允许跨域的http方法
corsConfiguration.addAllowedMethod("*");
// 设置允许跨域的请求头
corsConfiguration.addAllowedHeader("*");
// 允许带凭证
corsConfiguration.setAllowCredentials(true);
// 对所有的url生效
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
// 跨域支持
http.cors().configurationSource(corsConfigurationSource());
授权
url 安全表达式
// 权限集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 设置权限
if (user.getUsername().startsWith("admin")) {
// 给用户名是 admin 开头的用户 设置管理员权限
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
// 设置url 的访问权限 此处是登录成功后能够做的事情
// /demo/** 下的url 需要 admin 权限
// hasRole:指定需要特定的角色的用户允许访问, 会自动在角色前面插入'ROLE_'
http.authorizeRequests().antMatchers("/demo/**").hasRole("ADMIN");
自定义权限不足处理逻辑
@Service
public class DeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("权限不足,请联系管理员");
}
}
@Autowired
private DeniedHandler deniedHandler;
// 自定义权限不足的信息
http.exceptionHandling().accessDeniedHandler(deniedHandler);
自定义 bean 授权
@Service
public class AuthorizationService {
// 自定义权限认证
public boolean checkAuthority(Authentication authentication, HttpServletRequest request) {
User user = (User) authentication.getPrincipal();
Collection<GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities) {
String authorityName = authority.getAuthority();
if ("ROLE_ADMIN".equals(authorityName)) {
System.out.println(user.getUsername() + "有管理员权限");
return true;
}
}
return false;
}
}
// 自定义bean 权限认证http.authorizeRequests().antMatchers("/demo/**").access("@authorizationService.checkAuthority(authentication,request)");
method 安全表达式
开启方法级别的注解配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize
@RequestMapping("/findAll")
// 在进入方法前进行验证
@PreAuthorize("hasRole('ADMIN')")
public List<PayingUser> findAll() {
return payingUserDao.findAll();
}
@RequestMapping("/update/{id}")
@PreAuthorize("#id<10")
public String update(@PathVariable Integer id) {
return "针对id小于10的用户可访问";
}
@PostAuthorize
@GetMapping("/get/{id}")
// 可以拿到返回值 此处判断的权限是当前登录用户名和返回的用户名相同
@PostAuthorize("returnObject.username == authentication.principal.username")
public PayingUser get(@PathVariable Integer id) {
return payingUserDao.findById(id).get();
}
@PreFilter
@GetMapping("/delByIds")
// 可以对集合类型的参数进行过滤 将不符合条件的元素剔除
// http://127.0.0.1:8080/demo/delByIds?id=1,2,3,4,5,6,7
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")
public void delByIds(@RequestParam(value = "id") List<Integer> ids) {
for (Integer id : ids) {
System.out.println(id); // 2 4 6
}
}
@PostFilter
@GetMapping("/findAllPayUser")
// 对集合类型的返回值进行过滤
@PostFilter("filterObject.id % 2==0")
public List<PayingUser> findAllPayUser() {
return payingUserDao.findAll(); // 返回 id = 2,4,6... 的用户
}
RBAC 权限管理
需要五张表。
一个用户可以有多个角色。一个角色可以有多个权限。
-
用户表:
CREATE TABLE `paying_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) COLLATE utf8_bin DEFAULT NULL, `password` varchar(100) COLLATE utf8_bin DEFAULT NULL, `status` int(1) DEFAULT NULL COMMENT '用户状态1-启用 0-关闭', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=COMPACT;
-
角色表:
CREATE TABLE `t_role` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称', `ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`ID`) USING BTREE ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
-
权限表:
CREATE TABLE `t_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `permission_name` varchar(30) DEFAULT NULL COMMENT '权限名称', `permission_tag` varchar(30) DEFAULT NULL COMMENT '权限标签', `permission_url` varchar(100) DEFAULT NULL COMMENT '权限地址', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
-
用户-角色关系表:
CREATE TABLE `t_user_role` ( `UID` int(11) NOT NULL COMMENT '用户编号', `RID` int(11) NOT NULL COMMENT '角色编号', PRIMARY KEY (`UID`,`RID`) USING BTREE, KEY `FK_Reference_10` (`RID`) USING BTREE, CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`), CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `paying_user` (`id`) ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
-
角色-权限关系表:
CREATE TABLE `t_role_permission` ( `RID` int(11) NOT NULL COMMENT '角色编号', `PID` int(11) NOT NULL COMMENT '权限编号', PRIMARY KEY (`RID`,`PID`) USING BTREE, KEY `FK_Reference_12` (`PID`) USING BTREE, CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`), CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `t_permission` (`id`) ) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
根据用户 id 查询用户权限的 SQL。
SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,paying_user u
WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id = ?
// SecurityConfiguration 添加
// 基于数据库的权限认证
http.authorizeRequests().antMatchers("/demo/**").hasAuthority("user:findAll");
// PayingUserService 添加
List<Permission> permissions = permissionDao.findByUserId(user.getId());
for (Permission permission : permissions) {
// 根据数据库存到内容设置权限
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
参考git:https://gitee.com/zhangyizhou/learning-spring-security-demo.git