Bootstrap

Spring Security 6学习实践

一、集成JWT前后端分离认证

  1. 引入所需依赖jwt

  2. 实现 UserDetail,对应需要的数据, 实现UserDetailsService 数据库认证

  3. SecurityConfig 注入自定义认证管理器 AuthenticationManager,登录接口放行

  4. 登录接口,所有认证管理器(会调用UserDetailsService),认证成功生成jwt

  5. jwt授权管理器

1、配置

1-1、引入依赖

<!--JWT-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--工具包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0.M3</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

1-2、实现 UserDetail

import lombok.Data;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.authority.SimpleGrantedAuthority;  
import org.springframework.security.core.userdetails.UserDetails;  
  
import java.util.Collection;  
import java.util.List;  
import java.util.stream.Collectors;  
  
/**  
 * @author sjqn  
 * @date 2023/9/1  
 */@Data  
public class UserAuth implements UserDetails {  
  
    private String username; //固定不可更改  
    private String password;//固定不可更改  
    private String nickName;  //扩展属性  昵称  
    private List<String> roles; //角色列表  
  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        if(roles==null) {return null;}  
        //把角色类型转换并放入对应的集合  
        return roles.stream()  
                .map(role -> new SimpleGrantedAuthority("ROLE_"+role))  
                .collect(Collectors.toList());  
    }  
  
    @Override  
    public boolean isAccountNonExpired() {  
        return true;  
    }  
  
    @Override  
    public boolean isAccountNonLocked() {  
        return true;  
    }  
  
    @Override  
    public boolean isCredentialsNonExpired() {  
        return true;  
    }  
  
    @Override  
    public boolean isEnabled() {  
        return true;  
    }

1-3、jwt工具类

import cn.hutool.core.date.DateField;  
import cn.hutool.core.date.DateUtil;  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.JwtBuilder;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
  
import java.nio.charset.StandardCharsets;  
import java.util.Date;  
import java.util.Map;  
  
public class JwtUtil {  
    /**  
     * 生成jwt  
     * 使用Hs256算法, 私匙使用固定秘钥  
     *  
     * @param secretKey jwt秘钥  
     * @param dateOffset jwt过期时间(小时)  
     * @param claims    设置的信息  
     * @return  
     */  
    public static String createJWT(String secretKey , int dateOffset, Map<String, Object> claims) {  
        // 指定签名的时候使用的签名算法,也就是header那部分  
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  
  
        // 设置jwt的body  
        JwtBuilder builder = Jwts.builder()  
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的  
                .setClaims(claims)  
                // 设置签名使用的签名算法和签名使用的秘钥  
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))  
                // 设置过期时间  
                .setExpiration(DateUtil.offset(new Date(), DateField.HOUR_OF_DAY, dateOffset));  
  
        return builder.compact();  
    }  
  
    /**  
     * Token解密  
     *  
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个  
     * @param token     加密后的token  
     * @return  
     */  
    public static Claims parseJWT(String secretKey, String token) {  
        try {  
            // 得到DefaultJwtParser  
            Claims claims = Jwts.parser()  
                    // 设置签名的秘钥  
                    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))  
                    // 设置需要解析的jwt  
                    .parseClaimsJws(token).getBody();  
            return claims;  
        } catch (Exception e) {  
//            throw new AccessDeniedException("没有权限,请登录");  
            throw new RuntimeException("没有权限,请登录");  
        }  
    }  
  
}

2、实现

2-1、 Spring security注入认证管理器,放行登录接口

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
import org.springframework.security.crypto.password.PasswordEncoder;  
import org.springframework.security.web.SecurityFilterChain;  
  
/**  
 * @author sjqn  
 * @date 2023/10/19  
 */@Configuration  
public class SecurityConfig {  
  
    @Autowired  
    private TokenAuthorizationManager tokenAuthorizationManager;  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  
        http.authorizeHttpRequests().antMatchers("/security/login").permitAll()  
                .anyRequest().access(tokenAuthorizationManager);  
  
        //关闭session  
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);  
        //关闭缓存  
        http.headers().cacheControl().disable();  
  
        http.csrf().disable();  
        //返回  
        return http.build();  
  
    }  
  
  
    // 注入认证管理器:JWT使用  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {  
        return authenticationConfiguration.getAuthenticationManager();  
    }  
  
    @Bean  
    PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
}

2-2、登录接口

import com.itheima.project.dto.LoginDto;  
import com.itheima.project.util.JwtUtil;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.Authentication;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
import java.util.HashMap;  
import java.util.Map;  
  
@RestController  
@RequestMapping("security")  
public class LoginController {  
  
    @Autowired  
    AuthenticationManager authenticationManager;  
  
    @PostMapping("/login")  
    public String login(@RequestBody LoginDto loginDto) {  
        // 认证管理器  
        UsernamePasswordAuthenticationToken authentication  
                = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());  
        // 调用UserDetailServiceImpl,进行数据库校验  
        Authentication authenticate = authenticationManager.authenticate(authentication);  
  
        // 认证通过,生成jwt  
        if (authenticate.isAuthenticated()) {  
            Object principal = authenticate.getPrincipal();  
            Map<String, Object> claims = new HashMap<>();  
            claims.put("user", principal);  
            // (密钥,过期时间,内容)  
            String token = JwtUtil.createJWT("itcast", 360000, claims);  
            return token;  
        } else {  
            return "";  
        }  
    }  
}

2-3、登录认证

import com.itheima.project.entity.User;  
import com.itheima.project.mapper.UserMapper;  
import com.itheima.project.vo.UserAuth;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.core.userdetails.UserDetailsService;  
import org.springframework.security.core.userdetails.UsernameNotFoundException;  
import org.springframework.stereotype.Component;  
  
import java.util.ArrayList;  
import java.util.List;  
  
/**  
 * @author sjqn  
 * @date 2023/10/19  
 */@Component  
public class UserDetailServiceImpl implements UserDetailsService {  
  
    @Autowired  
    private UserMapper userMapper;  
  
    @Override  
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
        //查询用户  
        User user = userMapper.findByUsername(username);  
        if(user == null){  
            throw new RuntimeException("用户不存在或已被禁用");  
        }  
        UserAuth userAuth = new UserAuth();  
        userAuth.setUsername(user.getUsername());  
        userAuth.setPassword(user.getPassword());  
        userAuth.setNickName(user.getNickName());  
  
        //添加角色  
        List<String> roles=new ArrayList<>();  
        if("[email protected]".equals(username)){  
            roles.add("USER");  
            userAuth.setRoles(roles);  
        }  
        if("[email protected]".equals(username)){  
            roles.add("USER");  
            roles.add("ADMIN");  
            userAuth.setRoles(roles);  
        }  
        return userAuth;  
    }  
}

3、授权管理器

3-1、修改SecurityConfig,注册授权管理器

  • 并同时关闭session和缓存,前后端分离项目不需要使用session和缓存
     @Bean  
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
 ​  
         http.authorizeHttpRequests()
		         .antMatchers("/security/login").permitAll()  
                 .anyRequest().access(tokenAuthorizationManager);  
           
         //关闭session  
         http.sessionManagement()
	         .sessionCreationPolicy(SessionCreationPolicy.STATELESS);  
         //关闭缓存  
         http.headers().cacheControl().disable();  
           
         http.csrf().disable();  
         //返回  
         return http.build();  
     }

3-2、实现AuthorizationManager<RequestAuthorizationContext>

import cn.hutool.core.util.ObjectUtil;  
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.JSONObject;  
import com.itheima.project.util.JwtUtil;  
import com.itheima.project.vo.UserAuth;  
import io.jsonwebtoken.Claims;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.authorization.AuthorizationDecision;  
import org.springframework.security.authorization.AuthorizationManager;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;  
import org.springframework.stereotype.Component;  
  
import javax.servlet.http.HttpServletRequest;  
import java.util.function.Supplier;  
  
/**  
 * @author sjqn  
 * @date 2023/9/1  
 */@Component  
public class TokenAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {  
  
    @Override  
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {  
  
        // 获取request  
        HttpServletRequest request = requestAuthorizationContext.getRequest();  
        // 获取用户当前的请求地址  
        String requestURI = request.getRequestURI();  
        // 获取token  
        String token = request.getHeader("token");  
        if(null == token || "".equals(token)){  
            return new AuthorizationDecision(false);  
        }  
        // 解析token  
        Claims claims = JwtUtil.parseJWT("itcast", token);  
        if (ObjectUtil.isEmpty(claims)) {  
            // token失效  
            return new AuthorizationDecision(false);  
        }  
        // 获取userAuth  
        UserAuth userAuth = JSONObject.parseObject(JSON.toJSONString(claims.get("user")),UserAuth.class);  
        // 存入上下文  
        UsernamePasswordAuthenticationToken auth  
                =new UsernamePasswordAuthenticationToken( userAuth, userAuth.getPassword(), userAuth.getAuthorities());  
        SecurityContextHolder.getContext().setAuthentication(auth);  
  
        // 判断地址与对象中的角色是否匹配  
        if(userAuth.getRoles().contains("ADMIN")){  
            if("/hello/admin".equals(requestURI)){  
                return new AuthorizationDecision(true);  
            }  
        }  
        if(userAuth.getRoles().contains("USER")){  
            if("/hello/user".equals(requestURI)){  
                return new AuthorizationDecision(true);  
            }  
        }  
        return new AuthorizationDecision(false);  
    }  
}

二、Spring Security 6

功能:

  • 身份认证(authentication)

  • 授权(authorization)

  • 防御常见攻击(protection against common attacks)

身份认证:

  • 身份认证是验证谁正在访问系统资源,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。

授权:

  • 用户进行身份认证后,系统会控制谁能访问哪些资源,这个过程叫做授权。用户无法访问没有权限的资源。

防御常见攻击:

  • CSRF

  • HTTP Headers

  • HTTP Requests

二、自定义配置

0、密码加密方式

注入 WebSecurityConfig

@Bean 
public PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder(); 
}

1、基于内存的用户认证

创建一个WebSecurityConfig文件:

  • 定义一个@Bean,类型是UserDetailsService,实现是InMemoryUserDetailsManager
@Configuration  
//@EnableWebSecurity //开启security自定义配置 sp 自带  
public class WebSecurityConfig {  
  
    /**  
     * 创建基于内存的用户信息管理器  
     * @return  
     */  
    @Bean  
    public UserDetailsService userDetailsService() {  
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();  
        manager.createUser(  
                User  
                        .withDefaultPasswordEncoder()  
                        .username("like") //自定义用户名  
                        .password("like") //自定义密码  
                        .roles("USER") //自定义角色  
                        .build()  
        );  
        return manager;  
    }  
}

**测试:**使用用户名like,密码like

2、基于数据库的用户认证

  • 程序启动时:

    • 创建DBUserDetailsManager类,实现接口 UserDetailsManager, UserDetailsPasswordService

    • 在应用程序中初始化这个类的对象

  • 校验用户时:

    • SpringSecurity自动使用DBUserDetailsManagerloadUserByUsername方法从数据库中获取User对象

    • UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证

定义 DBUserDetailsManager

public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {  
      
    @Resource  
    private UserMapper userMapper;  
      
    /**  
     * 根据username获取用户信息  
     * @param username  
     * @return  
     * @throws UsernameNotFoundException  
     */    
     @Override  
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
        queryWrapper.eq("username",username);  
        User user = userMapper.selectOne(queryWrapper);  
        if (user == null) {  
            throw new UsernameNotFoundException(username);  
        } else {  
            Collection<GrantedAuthority> authorities = new ArrayList<>();  
            return new org.springframework.security.core.userdetails.User(  
                    user.getUsername(),  
                    user.getPassword(),  
                    user.getEnabled(),  
                    true, //用户账号是否过期  
                    true, //用户凭证是否过期  
                    true, //用户是否未被锁定  
                    authorities); //权限列表  
        }  
    }
    
######## 省略其他未定义方法
}

修改 WebSecurityConfig

  • 或者直接在DBUserDetailsManager类上添加@Component注解
@Configuration  
public class WebSecurityConfig {  
  
    /**  
     * 基于数据库认证  
     * @return  
     */  
    @Bean  
    public UserDetailsService userDetailsService() {  
        DBUserDetailsManager manager = new DBUserDetailsManager();  
        return manager;  
    }  
}

三、认证

引入 fastjson

Fastjson 是阿里巴巴开源的一个 JSON 解析库,是目前 Java 领域最快的 JSON 解析器之一。它提供了快速高效的 JSON 解析和生成功能,广泛用于 Java 开发中。以下是 Fastjson 的一些特点和用法:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.37</version>
</dependency>

一、WebSecurityConfig

==定义 SecurityFilterChain Bean

@Configuration  
public class WebSecurityConfig {  
  
    /**  
     * 定义 SecurityFilterChain Bean  
     * @param http  
     * @return  
     * @throws Exception  
     */    
     @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
  
        // 开启授权保护
        http.authorizeRequests(
                authorize -> authorize
                        /*
                        //具有USER_LIST权限的用户可以访问接口 /user/list
                        .requestMatchers("/user/list").hasAuthority("USER_LIST")
                        //具有USER_ADD权限的用户可以访问接口 /user/add
                        .requestMatchers("/user/add").hasAuthority("USER_ADD")
                        //具有管理员角色的用户可以访问 /user/**
                        .requestMatchers("/user/**").hasRole("ADMIN")
                        */
                        .anyRequest()
                        .authenticated()
        );
        // 登录 校验  
        http.formLogin(form -> {  
            form.loginPage("/login").permitAll()//登录页面无需授权即可访问  
                    // 配置自定义的表单信息,默认username, 无需修改  
                    .usernameParameter("myusername")  
                    .passwordParameter("mypassword")  
                    // 检验失败 跳转地址  
                    .failureUrl("/login?failure")  
                    // 用户登录 成功/失败 时的处理  
                    .successHandler(new MyAuthenticationSuccessHandler())  
                    .failureHandler(new MyAuthenticationFailureHandler())  
            ;  
        });  
        // 注销成功的处理
        http.logout(logout -> {  
            logout.logoutSuccessHandler(new MyLogoutSuccessHandler());  
        });  
        // 错误处理  
        http.exceptionHandling(exception -> {  
            // 未认证  
            exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());  
            // 未授权  
            exception.accessDeniedHandler(new MyAccessDeniedHandler());  
        });  
        //会话管理  
        http.sessionManagement(session -> {  
            session  
                    .maximumSessions(1) // 允许 1个账号 同时在线  
                    .expiredSessionStrategy(new MySessionInformationExpiredStrategy());  
        });  
        // 跨域  
        http.cors(withDefaults());  
        // 关闭 csrf 保护  
        http.csrf((csrf) -> {  
            csrf.disable();  
        });  
        return http.build();  
    }  
  
}

二、登录认证成功 调用方法

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {  
  
    /**  
     * 登录认证成功 调用  
     * @param request  
     * @param response  
     * @param authentication  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
  
  
        Object principal = authentication.getPrincipal(); // 获取用户身份信息  
        /*Object credentials = authentication.getCredentials(); // 获取用户凭证密码  
        // 获取用户权限信息  
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();*/  
        HashMap<String,Object> result = new HashMap<>();  
        result.put("code",0);  
        result.put("message","登录成功");  
        result.put("data",principal);  
  
        // 使用fastjson,将对象转 json        
        String json = JSON.toJSONString(result);  
  
        // 响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

三、登录认证失败 调用方法

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {  
  
    /**  
     * 登录认证失败 调用  
     * @param request  
     * @param response  
     * @param exception  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {  
  
        //获取错误信息  
        String localizedMessage = exception.getLocalizedMessage();  
  
        HashMap<String,Object> result = new HashMap<>();  
        result.put("code",-1);  
        result.put("message","登录失败");  
        result.put("data",localizedMessage);  
  
        // 使用fastjson,将对象转 json        String json = JSON.toJSONString(result);  
  
        // 响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

四、注销结果处理

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {  
  
    /**  
     *  注 销  
     * @param request  
     * @param response  
     * @param authentication  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
  
        //创建结果对象  
        HashMap result = new HashMap();  
        result.put("code", 0);  
        result.put("message", "注销成功");  
  
        //转换成json字符串  
        String json = JSON.toJSONString(result);  
  
        //返回响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

五、未认证处理

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {  
  
    /**  
     *  未认证 处理  
     * @param request  
     * @param response  
     * @param authException  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {  
        //获取错误信息  
        //String localizedMessage = authException.getLocalizedMessage();  
  
        //创建结果对象  
        HashMap result = new HashMap();  
        result.put("code", -1);  
        result.put("message", "需要登录");  
  
        //转换成json字符串  
        String json = JSON.toJSONString(result);  
  
        //返回响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

六、在Controller中获取用户信息

@RestController  
public class IndexController {  
  
    @GetMapping("/")  
    public Map index() {  
  
        //存储认证对象的上下文  
        SecurityContext context = SecurityContextHolder.getContext();  
        //认证对象  
        Authentication authentication = context.getAuthentication();  
        Object principal = authentication.getPrincipal(); // 身份  
        Object credentials = authentication.getCredentials(); // 凭证  
        // 权限  
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();  
        String name = authentication.getName(); // 用户名  
  
        HashMap<String, Object> result = new HashMap<>();  
        result.put("code", 0);  
        result.put("username", name);  
        result.put("authorities",authorities);  
  
  
        return result;  
    }  
}

七、会话并发处理

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {  
  
    /**  
     * 会话并发处理 : 后登录的账号会使先登录的账号失效  
     * @param event  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {  
        //创建结果对象  
        HashMap result = new HashMap();  
        result.put("code", -1);  
        result.put("message", "该账号已从其他设备登录");  
  
        //转换成json字符串  
        String json = JSON.toJSONString(result);  
  
        HttpServletResponse response = event.getResponse();  
        //返回响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

四、授权

一、未授权 处理

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {  
  
    /**  
     * 会话并发处理 : 后登录的账号会使先登录的账号失效  
     * @param event  
     * @throws IOException  
     * @throws ServletException  
     */    
     @Override  
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {  
        //创建结果对象  
        HashMap result = new HashMap();  
        result.put("code", -1);  
        result.put("message", "该账号已从其他设备登录");  
  
        //转换成json字符串  
        String json = JSON.toJSONString(result);  
  
        HttpServletResponse response = event.getResponse();  
        //返回响应  
        response.setContentType("application/json;charset=UTF-8");  
        response.getWriter().println(json);  
    }  
}

二、用户-权限-资源

需求:

  • 具有USER_LIST权限的用户可以访问/user/list接口

  • 具有USER_ADD权限的用户可以访问/user/add接口

配置权限

SecurityFilterChain

//开启授权保护
http.authorizeRequests(
        authorize -> authorize
    			//具有USER_LIST权限的用户可以访问/user/list
                .requestMatchers("/user/list").hasAuthority("USER_LIST")
    			//具有USER_ADD权限的用户可以访问/user/add
    			.requestMatchers("/user/add").hasAuthority("USER_ADD")
                //对所有请求开启授权保护
                .anyRequest()
                //已认证的请求会被自动授权
                .authenticated()
        );

硬编码 授权权限

  • ==DBUserDetailsManager中的loadUserByUsername方法:
/**  
 * 根据username获取用户信息  
 * @param username  
 * @return  
 * @throws UsernameNotFoundException  
 */
 @Override  
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
    queryWrapper.eq("username",username);  
    User user = userMapper.selectOne(queryWrapper);  
    if (user == null) {  
        throw new UsernameNotFoundException(username);  
    } else {  
        /*Collection<GrantedAuthority> authorities = new ArrayList<>();  
        // TODO 硬编码 添加权限  
      //authorities.add(()->"USER_LIST");        authorities.add(()->"USER_ADD");        return new org.springframework.security.core.userdetails.User(                user.getUsername(),                user.getPassword(),                user.getEnabled(),                true, //用户账号是否过期  
                true, //用户凭证是否过期  
                true, //用户是否未被锁定  
                authorities); //权限列表*/  
        return org.springframework.security.core.userdetails.User  
                .withUsername(user.getUsername())  
                .password(user.getPassword())  
                .roles("ADMIN")  
                .build();  
    }  
}

三、用户-角色-资料

SecurityFilterChain

 //开启授权保护  
 http.authorizeRequests(  
         authorize -> authorize  
                 //具有管理员角色的用户可以访问/user/**  
                 .requestMatchers("/user/**").hasRole("ADMIN")  
                 //对所有请求开启授权保护  
                 .anyRequest()  
                 //已认证的请求会被自动授权  
                 .authenticated()  
 );
授予角色

DBUserDetailsManager中的loadUserByUsername方法:

 return org.springframework.security.core.userdetails.User  
         .withUsername(user.getUsername())  
         .password(user.getPassword())  
         .roles("ADMIN")  
         .build();

四、用户-角色-权限-资源

RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例:

  1. 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
列名数据类型描述
user_idint用户ID
usernamevarchar用户名
passwordvarchar密码
emailvarchar电子邮件地址
  1. 角色表(Role table):存储所有可能的角色及其描述。
列名数据类型描述
role_idint角色ID
role_namevarchar角色名称
descriptionvarchar角色描述
  1. 权限表(Permission table):定义系统中所有可能的权限。
列名数据类型描述
permission_idint权限ID
permission_namevarchar权限名称
descriptionvarchar权限描述
  1. 用户角色关联表(User-Role table):将用户与角色关联起来。
列名数据类型描述
user_role_idint用户角色关联ID
user_idint用户ID
role_idint角色ID
  1. 角色权限关联表(Role-Permission table):将角色与权限关联起来。
列名数据类型描述
role_permission_idint角色权限关联ID
role_idint角色ID
permission_idint权限ID

在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。

当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。

五、注解授权

开启方法授权

在配置文件中添加如下注解 WebSecurityConfig

  • 其中 securedEnabled = true 表示启用 @Secured 注解,该注解可以标注在方法上,指定该方法需要某个角色或权限才能够访问。

  • prePostEnabled = true 表示启用 @PreAuthorize@PostAuthorize 注解,这两个注解也可以标注在方法上,用于在方法执行前或执行后进行权限校验。

@EnableMethodSecurity(securedEnabled = true,prePostEnabled = true)

给用户授予角色和权限

DBUserDetailsManager中的loadUserByUsername方法:

 return org.springframework.security.core.userdetails.User  
         .withUsername(user.getUsername())  
         .password(user.getPassword())  
         .roles("ADMIN")  
         .authorities("USER_ADD", "USER_UPDATE")  
         .build();
根据角色,用户名 鉴权
 //用户必须有 ADMIN 角色 并且 用户名是 admin 才能访问此方法  
 @PreAuthorize("hasRole('ADMIN') and authentication.name == 'admim'")  
 @GetMapping("/list")  
 public List<User> getList(){  
     return userService.list();  
 }  
根据用户权限 鉴权
 //用户必须有 USER_ADD 权限 才能访问此方法  
 @PreAuthorize("hasAuthority('USER_ADD')")  
 @PostMapping("/add")  
 public void add(@RequestBody User user){  
     userService.saveUserDetails(user);  
 }

二、OAuth2

1、OAuth2简介

1.1、OAuth2是什么

“Auth” 表示 “授权” Authorization

“O” 是 Open 的简称,表示 “开放”

连在一起就表示 “开放授权”,OAuth2是一种开放授权协议。

OAuth2最简向导:The Simplest Guide To OAuth 2.0

1.2、OAuth2的角色

OAuth 2协议包含以下角色:

  1. 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。

  2. 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。

  3. 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。

  4. 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.3、OAuth2的使用场景

开放系统间授权
社交登录

在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

开放API

例如云冲印服务的实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现代微服务安全
单块应用安全

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微服务安全

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

企业内部应用认证授权
  • SSO:Single Sign On 单点登录

  • IAM:Identity and Access Management 身份识别与访问管理

1.4、OAuth2的四种授权模式

RFC6749:

RFC 6749 - The OAuth 2.0 Authorization Framework (ietf.org)

阮一峰:

OAuth 2.0 的四种方式 - 阮一峰的网络日志 (ruanyifeng.com)

四种模式:

  • 授权码(authorization-code)

  • 隐藏式(implicit)

  • 密码式(password)

  • 客户端凭证(client credentials)

第一种方式:授权码

授权码(authorization code),指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 注册客户应用:客户应用如果想要访问资源服务器需要有凭证,需要在授权服务器上注册客户应用。注册后会获取到一个ClientID和ClientSecrets

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第二种方式:隐藏式

隐藏式(implicit),也叫简化模式,有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。

RFC 6749 规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为隐藏式。这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

https://a.com/callback#token=ACCESS_TOKEN
 将访问令牌包含在URL锚点中的好处:锚点在HTTP请求中不会发送到服务器,减少了泄漏令牌的风险。

第三种方式:密码式

密码式(Resource Owner Password Credentials):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌。

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第四种方式:凭证式

凭证式(client credentials):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.5、授权类型的选择

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2、Spring中的OAuth2

2.1、相关角色

**回顾:**OAuth 2中的角色

  1. 资源所有者(Resource Owner)

  2. 客户应用(Client)

  3. 资源服务器(Resource Server)

  4. 授权服务器(Authorization Server)

2.2、Spring中的实现

OAuth2 :: Spring Security

Spring Security

  • 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login

  • 资源服务器(OAuth2 Resource Server)

Spring

  • 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目。

2.3、相关依赖


 
  org.springframework.boot
  spring-boot-starter-oauth2-resource-server
 
 ​
 
 
  org.springframework.boot
  spring-boot-starter-oauth2-client
 
 ​
 
 
     org.springframework.boot
     spring-boot-starter-oauth2-authorization-server
 

2.4、授权登录的实现思路

使用OAuth2 Login

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、GiuHub社交登录案例

3.1、创建应用

注册客户应用:

登录GitHub,在开发者设置中找到OAuth Apps,创建一个application,为客户应用创建访问GitHub的凭据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

填写应用信息:默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。registrationId是ClientRegistration的唯一标识符。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

获取应用程序id,生成应用程序密钥:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.2、创建测试项目

创建一个springboot项目oauth2-login-demo,创建时引入如下依赖

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例代码参考:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples (github.com)

3.3、配置OAuth客户端属性

application.yml:

 spring:  
   security:  
     oauth2:  
       client:  
         registration:  
           github:  
             client-id: 7807cc3bb1534abce9f2  
             client-secret: 008dc141879134433f4db7f62b693c4a5361771b  
 #            redirectUri: http://localhost:8200/login/oauth2/code/github

3.4、创建Controller

 @Controller  
 public class IndexController {@GetMapping("/")  
     public String index(  
             Model model,  
             @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,  
             @AuthenticationPrincipal OAuth2User oauth2User) {  
         model.addAttribute("userName", oauth2User.getName());  
         model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());  
         model.addAttribute("userAttributes", oauth2User.getAttributes());  
         return "index";  
     }  
 }

3.5、创建html页面

resources/templates/index.html

 <!DOCTYPE html>  
 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">  
 <head>  
     <title>Spring Security - OAuth 2.0 Login</title>  
     <meta charset="utf-8" />  
 </head>  
 <body>  
 <div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">  
     <div style="float:left">  
         <span style="font-weight:bold">User: </span><span sec:authentication="name"></span>  
     </div>  
     <div style="float:none">&nbsp;</div>  
     <div style="float:right">  
         <form action="#" th:action="@{/logout}" method="post">  
             <input type="submit" value="Logout" />  
         </form>  
     </div>  
 </div>  
 <h1>OAuth 2.0 Login with Spring Security</h1>  
 <div>  
     You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>  
     via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>  
 </div>  
 <div>&nbsp;</div>  
 <div>  
     <span style="font-weight:bold">User Attributes:</span>  
     <ul>  
         <li th:each="userAttribute : ${userAttributes}">  
             <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>  
         </li>  
     </ul>  
 </div>  
 </body>  
 </html>

3.6、启动应用程序

  • 启动程序并访问localhost:8080。浏览器将被重定向到默认的自动生成的登录页面,该页面显示了一个用于GitHub登录的链接。

  • 点击GitHub链接,浏览器将被重定向到GitHub进行身份验证。

  • 使用GitHub账户凭据进行身份验证后,用户会看到授权页面,询问用户是否允许或拒绝客户应用访问GitHub上的用户数据。点击允许以授权OAuth客户端访问用户的基本个人资料信息。

  • 此时,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。

4、案例分析

4.1、登录流程

  1. A 网站让用户跳转到 GitHub,并携带参数ClientID 以及 Redirection URI。

  2. GitHub 要求用户登录,然后询问用户"A 网站要求获取用户信息的权限,你是否同意?"

  3. 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码。

  4. A 网站使用授权码,向 GitHub 请求令牌。

  5. GitHub 返回令牌.

  6. A 网站使用令牌,向 GitHub 请求用户数据。

  7. GitHub返回用户数据

  8. A 网站使用 GitHub用户数据登录

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2、CommonOAuth2Provider

CommonOAuth2Provider是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商(如Google、GitHub、Facebook)预定义了一组默认的属性。

例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。

因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。

```java
 GITHUB {
     public ClientRegistration.Builder getBuilder(String registrationId) {
         ClientRegistration.Builder builder = this.getBuilder(
         registrationId,
         ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
         
         //授权回调地址(GitHub向客户应用发送回调请求,并携带授权码)  
  “{baseUrl}/{action}/oauth2/code/{registrationId}”);
         builder.scope(new String[]{“read:user”});
         //授权页面
         builder.authorizationUri(“https://github.com/login/oauth/authorize”);
         //客户应用使用授权码,向 GitHub 请求令牌
         builder.tokenUri(“https://github.com/login/oauth/access_token”);
         //客户应用使用令牌向GitHub请求用户数据
         builder.userInfoUri(“https://api.github.com/user”);
         //username属性显示GitHub中获取的哪个属性的信息
         builder.userNameAttributeName(“id”);
         //登录页面超链接的文本
         builder.clientName(“GitHub”);
         return builder;
    }
 },




;