首页 > 其他分享 >SpringSecurity 学习记录

SpringSecurity 学习记录

时间:2022-12-10 23:24:08浏览次数:76  
标签:return String 记录 private SpringSecurity 学习 key id public

SpringSecurity简介

Spring Security,这是一种基于 Spring AOPServlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。本教程对 Spring Security 的使用进行一个比较全面的简要介绍。(摘自w3cschool)

创建工程与引入依赖

创建springboot项目或空maven项目

parent

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

dependencys

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
@RestController
@RequestMapping("/hello")
public class HelloController {
    @GetMapping
    public String hello(){
        return "hello , spring security!";
    }
}

启动项目访问/hello,会跳转到/login,这是默认的

流程

登录校验流程

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器。

登录拦截思路

登录

​ ①自定义登录接口

​ 调用ProviderManager的方法进行认证如果认证通过生成jwt

​ 把用户信息存入redis中

​ ②自定义UserDetailsService

​ 在这个实现类中去查询数据库
校验:

​ ①定义Jwt认证过滤器

​ 获取token

​ 解析token获取其中的userid

​ 从redis中获取用户信息

​ 存入SecurityContextHolder

引入redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

工具类与配置类

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheAutoConfiguration {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用fastJson
        template.setValueSerializer(fastJsonRedisSerializer);
        // hash的value序列化方式采用fastJson
        template.setHashValueSerializer(fastJsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    private final Class<T> clazz;

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    /**
     * 序列化
     */
    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (null == t) {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    /**
     * 反序列化
     */
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (null == bytes || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T) JSON.parseObject(str, clazz);
    }
}
public class JwtUtil {

    // 默认 生成之后有效时长 1小时
    private static final Long EXPIRE= 60 * 60 * 1000L;
    // 密钥明文
    private static final String JWT_KEY= "securityKey";// 不能有_

    public static String getUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "");
    }


    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @return String
     */
    public static String createJwt(String subject){
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }

    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @param expire 有效时间
     * @return String
     */
    public static String createJwt(String subject, Long expire){
        JwtBuilder builder = getJwtBuilder(subject, expire, getUUID());
        return builder.compact();
    }

    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @param expire 有效时间
     * @param id id
     * @return String
     */
    public static String createJwt(String subject, Long expire, String id){
        JwtBuilder builder = getJwtBuilder(subject, expire, id);
        return builder.compact();
    }


    /**
     *
     * @param subject subject
     * @param expire 过期时长 null 时 默认使用EXPIRATION,1小时
     * @param uuid uuid
     * @return JwtBuilder
     */
    private static JwtBuilder getJwtBuilder(String subject, Long expire, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (expire == null){
            expire = JwtUtil.EXPIRE;
        }
        long expMillis = nowMillis + expire;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)  // 唯一id
                .setSubject(subject)  // 主题,可以是json
                .setIssuer("ctp")  // 签发者
                .setIssuedAt(now)  // 签发时间
                .signWith(signatureAlgorithm, secretKey)  // 使用HS256算法签名,第二个时密钥
                .setExpiration(expDate);  // 过期时间
    }

    /**
     * 生成加密密钥
     * @return SecretKey
     */
    public static SecretKey generalKey(){
        byte[] decodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKeySpec key = new SecretKeySpec(decodeKey, 0, decodeKey.length, "AES");
        return key;
    }

    /**
     * 解析jwt
     * @param jwt jwt
     * @return Claims
     */
    public static Claims parseJwt(String jwt){
        SecretKey secretKey = generalKey();
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     * @return 缓存的对象
     */
    public ValueOperations<Object, Object> setCacheObject(Object key, Object value) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     * @return 缓存的对象
     */
    public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public Object getCacheObject(Object key) {
        ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key key
     */
    public void deleteObject(Object key) {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection collection
     */
    public void deleteObject(Collection<Object> collection) {
        redisTemplate.delete(collection);
    }

    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }

    public void expire(String key, int expire, TimeUnit timeUnit) {
        redisTemplate.expire(key, expire, timeUnit);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) {
        ListOperations<Object, Object> listOperations = redisTemplate.opsForList();
        if (null != dataList) {
            int size = dataList.size();
            for (Object o : dataList) {
                listOperations.leftPush(key, o);
            }
        }
        return listOperations;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public List<Object> getCacheList(String key) {
        List<Object> dataList = new ArrayList<>();
        ListOperations<Object, Object> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);
        if (null != size) {
            for (int i = 0; i < size; i++) {
                dataList.add(listOperation.index(key, i));
            }
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) {
        BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key);
        for (Object o : dataSet) {
            setOperation.add(o);
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key key
     * @return return
     */
    public Set<Object> getCacheSet(Object key) {
        Set<Object> dataSet = new HashSet<>();
        BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key key
     * @param dataMap dataMap
     * @return return
     */
    public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) {
        HashOperations<Object, Object, Object> hashOperations = redisTemplate.opsForHash();
        if (null != dataMap) {
            for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

    /**
     * 获得缓存的Map
     *
     * @param key key
     * @return return
     */
    public Map<Object, Object> getCacheMap(Object key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<Object> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}
public class WebUtil {

    /**
     * 将字符串渲染到客户端
     * @param resp resp
     * @param str str
     * @return return
     */
    public static String renderString(HttpServletResponse resp, String str){
        try{
            resp.setStatus(200);
            resp.setContentType("application/json");
            resp.setCharacterEncoding("utf-8");
            resp.getWriter().write(str);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Ret<T> {

    private Integer code;
    private String msg;
    private T data;

    public static Ret ok(){
        return new Ret(200,"ok",null);
    }

    public static Ret fail(){
        return new Ret(200,"ok",null);
    }

    public Ret data(T data){
        this.data = data;
        return this;
    }
}

引入mybatis-plus

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: "0101"
    url: jdbc:mysql://localhost:3306/db_learn?charsetEncoding=utf8&serverTimeZone=UTC

数据库表与实体类

CREATE TABLE `sec_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `role` tinyint DEFAULT NULL,
  `status` tinyint DEFAULT NULL,
  PRIMARY KEY (`id`)
) 
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sec_user")
public class User implements Serializable {
    private static final long serialVersionUID = -4445259547431031783L;
    @TableId
    private Long id;
    private String name;
    private String password;
    private Integer role;
    private Integer status;
}

测试mybatisplus

@Test
public void testMybatisPlus(){
    List<User> users = userMapper.selectList(null);
    System.out.println(users);
}

校验用户

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getName, username);
        User user = userMapper.selectOne(queryWrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }

        // TODO 查询用户权限信息

        // 把数据封装成UserDetail并返回
        return new LoginUser(user);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private static final long serialVersionUID = -3470371303923656730L;
    private User user;
    /**
     *
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getName();
    }

    /**
     *
     * @return true -> 登录状态未过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     *
     * @return true -> 帐户未锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     *
     * @return true -> 签发的凭据未过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     *
     * @return true -> 该用户是正常的
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如

密码问题PasswordEncoder

实际项目中我们不会把密码明文存储在数据库中。

默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password。 它会根据id去判断密码的加密方式。但是我们一般不会采这种方式。所以就需要替换

PasswordEncoder.

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就 会使用该PasswordEncoder来进行密码校验。

我们可以定义-个SpringSecurity的配置类,SpringSecurity要求这 个配置类要继承WebSecurityConfigurerAdapter。

关于PasswordEncoder,每次加密的盐值不一样,相同的密码最后生成的密文不一样

加密

PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
System.out.println(encode1);
System.out.println(encode2);

输出

$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK
$2a$10$4.XsTRDY.U0AEPESe8ZZxu4VoCv3UnrrAejnQp.xC6EcDO6jmCQEu

比对

boolean b1 = passwordEncoder
        .matches("123456", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
boolean b2 = passwordEncoder
        .matches("1234", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
System.out.println(b1);// true
System.out.println(b2);// false

安装Redis for windows

安装 redis for windows

https://blog.csdn.net/qq_52385631/article/details/122771598

使用教程

https://blog.csdn.net/weixin_61594803/article/details/122695446

大概步骤

1.下载 redis for windows

2.安装,安装,一路next

(密码设置 安装目录下 redis.windows-service.conf 找到 requirepass ,并设置密码,格式 requirepass xxx)

3.启动redis服务,此电脑->管理->服务

4.安装目录下启动redis.cli,界面显示主机名端口

登录接口

自定义登登录接口,然后让SpringSecurity对这 个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的

authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

认证成功的话要生成一个jwt, 放入响应中返回。并粗为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为

key

SecurityConfig重写两个方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // 关闭csrf
        .csrf().disable()
        // 不通过Session获取SecurityContext
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 对于登录接口,允许匿名访问
        .antMatchers("/user/login").anonymous()
        // 出上面以外,其他全要验证
        .anyRequest().authenticated();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
@RestController
public class LoginController {
    @Resource
    private LoginService loginService;
    @PostMapping("/user/login")
    public Ret login(@RequestBody User user){
        return loginService.login(user);
    }
}
@Service
public class LoginServiceImpl implements LoginService {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisUtil redisUtil;
    @Override
    public Ret login(User user) {
        // AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
        Authentication authentication = authenticationManager.authenticate(authenticationToken);// 调用UserDetailServiceImpl.loadUserByUsername认证
        // 未通过则给相应的提示
        if (Objects.isNull(authentication)) {
            throw new RuntimeException("登录失败");
        }
        // 认证通过,使用用户的id生成一个jwt
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long id = loginUser.getUser().getId();
        String jwt = JwtUtil.createJwt(id.toString());
        // 完整信息存入redis,用户的id作为key
        redisUtil.setCacheObject("login:" + id, loginUser);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        return Ret.ok().data(map);
    }
}

访问 http://localhost:8080/user/login

前提需要redis服务,否则失败

{
    "code": 200,
    "msg": "ok",
    "data": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns"
    }
}

检验token

Claims claims = JwtUtil.parseJwt("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns");
String subject = claims.getSubject();
System.out.println(subject);// 1 (iuserId)

上面代码可用获取到用户id

token认证过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // 获取token
        String token = httpServletRequest.getHeader("token");
        if (StringUtils.isEmpty(token)){
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        // 解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJwt(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("无效token");
        }

        // 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = (LoginUser)redisUtil.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        // 存入SecurityHolder
        // TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);// authorities表示授权信息
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

添加到配置SecurityConfig.configure

@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

退出登录接口

LoginServiceImpl

public Ret logout() {
    // 获取SecurityContextHolder中的用户信息
    UsernamePasswordAuthenticationToken authenticationToken =
            (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
    Long userId = loginUser.getUser().getId();
    // 删除redis中对应用户的信息
    redisUtil.deleteObject("login:" + userId);
    return Ret.ok().data("退出成功");
}

SecurityConfig

HttpSecurity.antMatchers("/user/login").anonymous()// 未登录可访问,登录不可访问(携带token,500错误)
HttpSecurity.antMatchers("/user/login").permitAll()// 不管是否登录都可以访问
HttpSecurity.anyRequest().authenticated(); // 登陆了就可以访问

授权引入

基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的

Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入

Authentication。然后设置我们的资源所需要的权限即可。

模拟授权

UserDetailServiceImpl.loadUserByUsername

...
// TODO 查询用户权限信息
List<String> list= new ArrayList<>(Arrays.asList("login","admin"));
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);

JwtAuthenticationTokenFilter.doFilterInternal

// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// authorities表示授权信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

配置SecurityConfig

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

LoginController权限注解,下面表示有hello权限才能访问接口

@PostMapping("/hello")
@PreAuthorize("hasAnyAuthority('hello')")
public Ret hello(){
   ...
}

数据库权限与接口访问

数据库表

sec_user

role

user_role

permission

role_permission

根据用户id查询权限sql语句示例

select distinct `p`.`name`
from `user_role` ur
	left join `role_permission` rp on `ur`.role_id = `rp`.role_id
	left join `permission` p on `p`.id =  `rp`.permisson_id
where `ur`.user_id = 1 and `p`.`name` is not null

-- 根据用户id 查询出角色id,然后根据角色id查询出拥有的权限id,再根据权限id查询出可用菜单集合(去重?用户可用有多个角色身份,可能存在权限重复)

PermissionMapper.java

List<String> selectPermsByUserId(@Param("userId") Long userId);

PermissionMapper.xml

<?xm1 version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learnspringsecurity.mapper.PermissionMapper">

    <select id="selectPermsByUserId" resultType="java.lang.String">
        select distinct `p`.`name`
        from `user_role` ur
            left join `role_permission` rp on `ur`.role_id = `rp`.role_id
            left join `permission` p on `p`.id =  `rp`.permisson_id
        where `ur`.user_id = #{userId} and `p`.`name` is not null
    </select>
</mapper>

给相应接口定义所需要的权限

@RequestMapping("/hi")
@PreAuthorize("hasAnyAuthority('hi')")
public String hello(){
    return "hi ~ ";
}

UserDetailServiceImpl.loadUserByUsername

List<String> list = permissionMapper.selectPermsByUserId(user.getId());
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);

自定义失败处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败

还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即呵。

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        // 授权失败 异常处理
        Ret<String> ret = new Ret<>(HttpStatus.FORBIDDEN.value(),"当前用户没有权限执行该操作",null);
        String jsonString = JSON.toJSONString(ret);
        WebUtil.renderString(httpServletResponse, jsonString);
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 认证失败 异常处理
        Ret<String> ret = new Ret<>(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请重新登录",null);
        String jsonString = JSON.toJSONString(ret);
        WebUtil.renderString(httpServletResponse, jsonString);
    }
}

SecurityConfig.configure

// 认证/授权 失败处理器
http.exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint)
        .accessDeniedHandler(accessDeniedHandler);

跨域

@Configuration
public class CrosConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 允许跨域请求
        registry.addMapping("/**")  // 允许的路径
                .allowedOrigins("*")  // 允许的域名
                .allowCredentials(true)  // 允许cookie?
                .allowedMethods("GET","POST","DELETE","PUT");// 允许的请求方式
    }
}

SecurityConfig.configure

// 允许跨域
http.cors();

权限校验方法

public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
    protected final Authentication authentication;
    private AuthenticationTrustResolver trustResolver;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";
    public final boolean permitAll = true;
    public final boolean denyAll = false;
    private PermissionEvaluator permissionEvaluator;
    public final String read = "read";
    public final String write = "write";
    public final String create = "create";
    public final String delete = "delete";
    public final String admin = "administration";
    public SecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        } else {
            this.authentication = authentication;
        }
    }
    public final boolean hasAuthority(String authority) {
        return this.hasAnyAuthority(authority);
    }
    public final boolean hasAnyAuthority(String... authorities) {
        return this.hasAnyAuthorityName((String)null, authorities);
    }
    public final boolean hasRole(String role) {
        return this.hasAnyRole(role);
    }
    public final boolean hasAnyRole(String... roles) {
        return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
    }
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;
        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        return false;
    }
    public final Authentication getAuthentication() {
        return this.authentication;
    }
    public final boolean permitAll() {
        return true;
    }
    public final boolean denyAll() {
        return false;
    }
    public final boolean isAnonymous() {
        return this.trustResolver.isAnonymous(this.authentication);
    }
    public final boolean isAuthenticated() {
        return !this.isAnonymous();
    }
    public final boolean isRememberMe() {
        return this.trustResolver.isRememberMe(this.authentication);
    }
    public final boolean isFullyAuthenticated() {
        return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication);
    }
    public Object getPrincipal() {
        return this.authentication.getPrincipal();
    }
    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }
    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }
    public void setDefaultRolePrefix(String defaultRolePrefix) {
        this.defaultRolePrefix = defaultRolePrefix;
    }
    private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return this.roles;
    }
    public boolean hasPermission(Object target, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
    }
    public boolean hasPermission(Object targetId, String targetType, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
    }
    public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }
    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        } else if (defaultRolePrefix != null && defaultRolePrefix.length() != 0) {
            return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role;
        } else {
            return role;
        }
    }
}

自定义权限校验方法

@Component("customExpressionRoot")
public class CustomExpressionRoot {

    public boolean hasAuthority(String authority){
        // 获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        // 判断是否存在对应权限
        return permissions.contains(authority);
    }
}

在SPEL表达式中使用@ex相当于获取容器中bean的名字为customExpressionRoot的对象。然后再调用这个对象的hasAuthority方法

@RequestMapping("/custom")
@PreAuthorize("@customExpressionRoot.hasAuthority('custom')")
public Ret custom(){
    return Ret.ok();
}

通过配置文件

接口路径与所需权限

HttpSecurity.antMatchers("/hi").hasAuthority("hi")

CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

https://blog.csdn.net/freeking101/article/details/86537087

SpringSecurity却防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf. token,前端发起请求的时候需要携带这个csrf_ token,后端会有过滤器进行校验,

如果没有携带或者是伪造的就不允许访问。我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,

而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

认证成功处理

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(authenticationSuccessHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
    /**
     * 作为bean放入容器中
     * @return AuthenticationManager
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功了");
        System.out.println(authentication.getPrincipal().toString());
    }
}

当提交这个表单

输出

认证成功了
LoginUser(...

认证失败处理器

@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("认证失败了");
    }
}
...
@Resource
private AuthenticationFailureHandler authenticationFailureHandler;
...
HttpSecurity.failureHandler(authenticationFailureHandler);

登出成功处理器

HttpSecurity.logout().logoutSuccessHandler(logoutSuccessHandler);
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登出成功");
   }
}

learn form https://www.bilibili.com/video/BV1mm4y1X7Hc

标签:return,String,记录,private,SpringSecurity,学习,key,id,public
From: https://www.cnblogs.com/piggyyy/p/16972589.html

相关文章

  • Java数组学习
    P51什么是数组数组的定义数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。其中每一个数据称作一个数组元素每个数......
  • 树链剖分学习笔记
    树链剖分学习笔记简介树链剖分是一种可以把树丢到线段树上维护的一种算法,时间复杂度为\(O(n\log^2n)\)。思路一、一些概念1.重儿子:如果一个点有儿子,那么所有儿子中......
  • tensorflow的官方强化学习库agents的一些注意事项
    源代码地址:https://github.com/tensorflow/agents  ====================================  1.gym的环境版本有要求,给出具体安装及Atari的安装:pipinstallgy......
  • 《深入理解计算机系统》第二章学习笔记
    补码编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表示时,某些运算......
  • 深度学习-循环神经网络(RNN)
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • MarkDowm学习
    Markdown学习标题字体Hello,World!Hello,World!Hello,World!Hello,World!引用学习Java,走向人生巅峰分割线图片 超链接点击此处起飞列表abca......
  • 2022-2023-1 20221305 《计算机基础与程序设计》第11周学习总结
    学期(如2022-2023-1)学号(如:20221300)《计算机基础与程序设计》第11周学习总结作业信息这个作业属于哪个课程<班级的链接>(如2022-2023-1-计算机基础与程序设计)这......
  • 2022-2023-1 20221305 《计算机基础与程序设计》第十周学习总结
    学期(如2022-2023-1)学号(如:20221300)《计算机基础与程序设计》第X周学习总结作业信息这个作业属于哪个课程<班级的链接>(如2022-2023-1-计算机基础与程序设计)这个......
  • 2022-2023-1 20221305 《计算机基础与程序设计》第13周学习总结
    2022-2023-120221305《计算机基础与程序设计》第13周学习总结作业信息这个作业属于哪个课程<班级的链接>(如2022-2023-1-计算机基础与程序设计)这个作业要求在......
  • delphi D11编程语言手册 学习笔记(P393-419) 对象与内存
      这本书可以在Delphi研习社②群256456744的群文件里找到.书名:Delphi11AlexandriaEdition.pdf 这些年来,Delphi行动装置编译器提供了一个不同的内存模式,称......