当前位置: 首页 > news >正文

【精品】SpringSecurity在前后端分离项目中的应用

相关博客

  • 环境搭建
    https://hcshow.blog.csdn.net/article/details/117083416

  • 自定义登录逻辑
    https://hcshow.blog.csdn.net/article/details/122347403
    https://hcshow.blog.csdn.net/article/details/122347650

  • 自定义登录页面的退出页面
    https://hcshow.blog.csdn.net/article/details/122349702

  • 自定义表单请求参数
    https://hcshow.blog.csdn.net/article/details/122364737

  • 放行静态资源
    https://hcshow.blog.csdn.net/article/details/122364853

  • 自定义403页面
    https://hcshow.blog.csdn.net/article/details/122365642

  • 整个Jwt/前后端分离(重点)
    https://hcshow.blog.csdn.net/article/details/122554517
    https://blog.csdn.net/lianghecai52171314/article/details/122434478 (带代码)

原理

在这里插入图片描述

由默认用户登录得到SpringSecurity下用户登录的请求流程:

入门示例:使用SpringSecurity提供的登录页面,实现真正的登录功能

第一步:自定义UserDetailsService

@Service
public class WegoUserDetailsService implements UserDetailsService {
    @Resource
    private MemberMapper memberMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //认证:根据用户名去查询对应的用户信息
        LambdaQueryWrapper<Member> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Member::getUsername,username);
        Member member = memberMapper.selectOne(queryWrapper);
        //如果没有查询到用户抛出异常
        if(member == null){
            throw GlobalException.builder().code(401).msg("用户名或密码不正确").build();
        }

        // TODO: 授权:查询用户的权限信息

        // 将用户信息+权限信息封装成UserDetails对象
        LoginMemberDetails loginMemberDetails = new LoginMemberDetails();
        loginMemberDetails.setMember(member);
        return loginMemberDetails;
    }
}

第二步:自定义UserDetails:

public class LoginMemberDetails implements UserDetails {
    @Setter
    private Member member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * 登录密码
     * @return
     */
    @Override
    public String getPassword() {
        return member.getPassword();
    }

    /**
     * 登录账户
     * @return
     */
    @Override
    public String getUsername() {
        return member.getUsername();
    }

    /**
     * 返回对象是否未过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

第三步:测试

为了测试,修改待登录用户的密码为:{noop}明文密码,比如:
在这里插入图片描述

密码加密存储

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password。它会根据id去判断密码的加密方式,但是我们一般不会采用这种方式,所以就需要替换PasswordEncoder。
实际项目中推荐使用BCryptPasswordEncoder。只需要把BCryptPasswordEncoder对象注入到Spring容器中,SpringSecurity就会使用该PasswordEncoder进行密码加密。
创建SpringSecurity配置文件,在其中指定加密方式:

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码加密器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

将数据库中的密码改成加密后的内容,然后测试就可以了。

用户登录:不再使用默认的用户user+随机字符串登录,使用我们自己数据库中的用户名+密码登录。

SpringSecurity需要对自定义的登录接口放行,让用户可以直接访问到这个接口。
在登录接口中需要通过AuthenticationManager的authenticate()方法进行用户认证,故需要先在SpringSecurity配置文件中把AuthenticationManager注入容器。
认证成功后生成一个jwt,在登录成功后的响应中返回。
为了让用户下回请求时能够通过jwt快速识别出用户,我们将用户信息缓存到Redis中,将用户的id作为key。

第一步:自定义UserDetails类:

@Getter
@Setter
@ToString
public class WegoUserDetails implements UserDetails {
    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

    @Override
    public String getUsername() {
        if(user == null){
            return null;
        }
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

第二步:自定义UserDatailsService

在其中实现根据用户名查找用户,并将用户的信息封装成我们自己的UserDatails对象并返回

@Service
public class WegoUserDetailsService implements UserDetailsService {
    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(Wrappers.<User>query().eq("username", username));
        //TODO:查询权限信息

        WegoUserDetails userDetails = new WegoUserDetails();
        userDetails.setUser(user);

        return userDetails;
    }
}

第三步:修改SpringSecurity配置类

在系统注入AuthenticationManager对象并放行登录接口user/login:

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 认证用:最终调用DetailsService的具体的登录逻辑
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //禁用Session:不通过Sessin获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //登录接口:匿名访问(未登录可以访问,登录的话不就能访问了)
                .antMatchers("/user/login").anonymous() //放行
                //其他的任何请求都需要鉴权认证
                .anyRequest().authenticated()
        ;
    }
}

第四步:自定义登录接口:

登录接口实现功能:
1、 将用户名+密码封装成UsernamePasswordAuthenticationToken对象
2、 调用AuthenticationManager的authenticate()方法进行登录判断

@RestController
@RequestMapping("/user")
public class LoginController {
    @Resource
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        UsernamePasswordAuthenticationToken passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        authenticationManager.authenticate(passwordAuthenticationToken);
        return ResultUtil.success();
    }
}

第五步:在SpringSecurity配置文件中注入加密类的对象:

/**
 * 密码加密器
 * @return
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

在测试类中,利用该对象生成密码并保存到数据库中

class FafuRbacApplicationTests {
    @Test
    public void fun() {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        System.out.println(passwordEncoder.encode("1234"));
    }
}

第六步:测试

打开postman,在其中请求user/login,注意以post方式+application/json的形式请求。

返回token

第一步:准备JWT工具类:

@Slf4j
@Component
//@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {
    /**
     * 携带JWT令牌的HTTP的Header的名称,在实际生产中可读性越差越安全
     */
    @Getter
    @Value("${jwt.token}")
    private String token;

    /**
     * 为JWT基础信息加密和解密的密钥
     * 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
     */
    @Value("${jwt.secret}")
    private String secret;

    /**
     * JWT令牌的有效时间,单位秒
     * - 默认2周
     */
    @Value("${jwt.expiration}")
    private Long expiration;

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

    /**
     * SecretKey 根据 SECRET 的编码方式解码后得到:
     * Base64 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
     * Base64URL 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
     * 未编码:SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
     */
    private static SecretKey getSecretKey(String secret) {
        byte[] encodeKey = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(encodeKey);
    }

    /**
     * 用claims生成token
     *
     * @param claims 数据声明,用来创建payload的私有声明,subject只是claims的一部分
     * @return token 令牌
     */
    private JwtBuilder getJwtBuilder(Map<String, Object> claims) {
        SecretKey key = getSecretKey(secret);
        //SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //两种方式等价

        // 添加payload声明
        JwtBuilder jwtBuilder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(getUUID())
                // 你也可以改用你喜欢的算法,支持的算法详见:https://github.com/jwtk/jjwt#features
                // SignatureAlgorithm.HS256:指定签名的时候使用的签名算法,也就是header那部分
                .signWith(key, SignatureAlgorithm.HS256)
                // iat: jwt的签发时间
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + this.expiration * 1000));
        return jwtBuilder;
    }

    /**
     * 生成Token令牌
     *
     * @param username 用户名
     * @return 令牌Token
     */
    public String generateToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", username);
        claims.put("created", new Date());
        return getJwtBuilder(claims).compact();
    }

    public String generateToken(Serializable id) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", id);
        claims.put("created", new Date());
        return getJwtBuilder(claims).compact();
    }

    /**
     * 从token中获取数据声明claim
     *
     * @param token 令牌token
     * @return 数据声明claim
     */
    public Claims getClaimsFromToken(String token) {
        try {
            SecretKey key = getSecretKey(secret);
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token).getBody();
            return claims;
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
            log.error("token解析错误", e);
            throw new IllegalArgumentException("Token invalided.");
        }
    }

    /**
     * 从token中获取登录用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getSubjectFromToken(String token) {
        String subject;
        try {
            Claims claims = getClaimsFromToken(token);
            subject = claims.getSubject();
        } catch (Exception e) {
            subject = null;
        }
        return subject;
    }

    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationFromToken(String token) {
        return getClaimsFromToken(token).getExpiration();
    }

    public String getUserRole(String token) {
        return (String) getClaimsFromToken(token).get("role");
    }

    /**
     * 判断token是否过期
     *
     * @param token 令牌
     * @return 是否过期:已过期返回true,未过期返回false
     */
    public Boolean isTokenExpired(String token) {
        Date expiration = getExpirationFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 验证令牌:判断token是否非法
     *
     * @param token       令牌
     * @param username 用户名
     * @return 如果token未过期且合法,返回true,否则返回false
     */
    public Boolean validateToken(String token, String username) {
        //如果已经过期返回false
        if (isTokenExpired(token)) {
            return false;
        }
        String usernameFromToken = getSubjectFromToken(token);
        return username.equals(usernameFromToken);
    }

}

第二步:修改LoginController

在login()方法中,当用户登录成功时,生成token,并放到返回结果中

@RestController
@RequestMapping("/user")
public class LoginController {
    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        UsernamePasswordAuthenticationToken passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(passwordAuthenticationToken);
        if(authenticate == null){
            return ResultUtil.error(401,"error");
        }
        user = ((WegoUserDetails)authenticate.getPrincipal()).getUser();
        String token = jwtUtil.generateToken("user:" + user.getId());

        return ResultUtil.success().addData("token",token);
    }
}

第三步:修改application.yml,其中添加jwt的配置信息:

jwt:
  # 为JWT基础信息加密和解密的密钥,长度需要大于等于43
  # 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改
  secret: oQZSeguYloAPAmKwvKqqnifiQatxMEPNOvtwPsCLasd
  # JWT令牌的有效时间,单位秒,默认2周
  expiration: 1209600
  token: Authorization

第四步:测试:

token认证过滤器

第一步:自定义过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private ByteRedisUtil<WegoUserDetails> wegoUserDetailsByteRedisUtil;

    @Resource
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //1、获取用户请求时传递过来的token
        String token = httpServletRequest.getHeader("Authorization");
        if(token == null || token.length() == 0){
            //放行到过滤器链中的下一个过滤器过滤
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }
        //如果用户请求携带有token,解析token
        Claims claimsFromToken = null;
        try {
            claimsFromToken = jwtUtil.getClaimsFromToken(token);
        } catch (Exception e) {
            System.out.println("非法的token");
            e.printStackTrace();
        }
        //获取用户id
        String userId = claimsFromToken.getSubject();
        //根据用户id,构造出一个Authentication对象,给过滤器链中后面的Filter使用(过滤器链中处理的都是Authentication对象)
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userId,null,null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        //放行到过滤器链中的下一个过滤器过滤
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

}

第二步:在SpringSecurity配置类中配置自定义的过滤器

@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
    ……
    //添加过滤器
    http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

第三步:测试

1、 先登录,获取token
2、 将token配置到header中,请求任意一个资源

完善token认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private ByteRedisUtil<WegoUserDetails> wegoUserDetailsByteRedisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //……..        //获取用户id
        String userId = claimsFromToken.getSubject();

        //从Redis中获取缓存的用户数据
        WegoUserDetails wegoUserDetails = wegoUserDetailsByteRedisUtil.get("user:" + userId);
        if(wegoUserDetails == null){
            System.out.println("无效的token");
        }


        //根据用户id,构造出一个Authentication对象,给过滤器链中后面的Filter使用(过滤器链中处理的都是Authentication对象)
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(wegoUserDetails,null,null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        //放行到过滤器链中的下一个过滤器过滤
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

}

异常处理

原理(套路性内容)

在SpringSecurity中,在认证或者授权的过程中出现的异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断这个异常是认证失败还是授权失败产生的:
  • 认证过程中出现的异常,会被封装成AuthenticationException,SpringSecurity会调用AuthenticationEntryPoint对象的方法处理这个异常
  • 授权过程中出现的异常,会被封装成AccessDeniedException,SpringSecurity会调用AccessDeniedHandler对象的方法处理这个异常
    所以,自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHanler,然后在SpringSecurity中进行配置即可。

Spring Security 中的异常主要分为两大类:一类是认证异常,另一类是授权相关的异常:

  • AuthenticationException 是在用户认证的时候出现错误时抛出的异常。系统用户不存在、被锁定、凭证失效、密码错误等认证过程中出现的异常都由 AuthenticationException 处理。主要的子类如图:
    在这里插入图片描述

  • AccessDeniedException 主要是在用户在访问受保护资源时被拒绝而抛出的异常。主要是 CSRF 相关的异常和授权服务异常。主要的子类如图:
    在这里插入图片描述

状态码:

  • 401 未授权状态
    HTTP 401 错误 - 未授权(Unauthorized) 一般来说该错误消息表明您首先需要登录(输入有效的用户名和密码)。 如果你刚刚输入这些信息,立刻就看到一个 401 错误,就意味着,无论出于何种原因您的用户名和密码其中之一或两者都无效(输入有误,用户名暂时停用,账户被锁定,凭证失效等) 。总之就是认证失败了。其实正好对应我们上面的 AuthenticationException 。

  • 403 被拒绝状态
    HTTP 403 错误 - 被禁止(Forbidden) 出现该错误表明您在访问受限资源时没有得到许可。服务器理解了本次请求但是拒绝执行该任务,该请求不该重发给服务器。并且服务器想让客户端知道为什么没有权限访问特定的资源,服务器应该在返回的信息中描述拒绝的理由。一般实践中我们会比较模糊的表明原因。 该错误对应了我们上面的 AccessDeniedException 。

示例

第一步:自定义认证失败处理器

/**
 * 认证失败处理器
 *
 * @author hc
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        String message = authException.getMessage();
        if (message == null){
            message = "登录失败";
        }
        String json = "{\"code\":\"401\",\"msg\":"+ message +"}";
        response.getWriter().write(json);
    }

}

第二步:自定义授权失败处理器

/**
 * 授权失败处理器
 *
 * @author hc
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        String message = accessDeniedException.getMessage();
        if (message == null){
            message = "您的权限不足";
        }
        String json = "{\"code\":\"403\",\"msg\":"+ message +"}";
        response.getWriter().write(json);
    }
}

第三步:在SpringSecurity配置文件中配置:

@Resource
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    ……
    //处理异常处理器
    http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint) //认证失败处理器
            .accessDeniedHandler(accessDeniedHandler) //授权失败处理器
    ;
}

第四步:测试。

1、 将Redis中的用户删除,然后请求某个资源

2、 错误的用户名or密码登录

退出登录

在控制器中提供一个退出登录的接口,然后获取SecurityContextHolder中的认证信息,最后删除Redis中对应的数据即可。

代码实现

@RestController
@RequestMapping("/user")
public class LogoutController {
    @Resource
    private ByteRedisUtil<WegoUserDetails> redisUtil;

    @GetMapping("/logout")
    public String logout(){
        //获取SecurityContextHolder中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        WegoUserDetails userDatails = (WegoUserDetails) authentication.getPrincipal();
        Long userId = userDatails.getUser().getId();
        //从Redis缓存中删除指定id的用户
        redisUtil.del(GlobalConst.REDIS_USER_KEY+userId);
        return "{\"code\":200,\"msg\":\"注销成功!\"}";
    }
}

测试

1、 正确用户名和密码登录,发现redis中缓存了数据
2、 访问某个资源,ok
3、 退出登录,发现Redis中缓存的数据没有了
4、 再次访问相同的资源,发现访问不到了:

授权

原理

SpringSecurity使用默认的FilterSecurityInterceptor进行权限校验。FilterSecurityInterceptor会从SecurityContextHolder获取 Authentication,然后获取其中的权限信息,从而知道当前用户是否拥有访问指定资源所需要的权限。
所以我们需要将当前登录用户的权限信息也存入到Authenticaion中。
只需要将用户的“权限+角色”信息存放到Authentication(在过滤器链中处理的对象)中,SpringSecurity就能自动处理权限验证

示例

准备工作

第一步:在RoleMapper.java接口中添加如下根据用户id获取该用户拥有的role的方法:
/**
 * 查询指定id的用户的role
 * @param userId
 * @return 角色名称-角色编码
 */
@Select("""
            SELECT tb_role.id, tb_role.`code`
            FROM tb_role INNER JOIN tb_user_role
            ON tb_role.id = tb_user_role.role_id
            WHERE tb_user_role.user_id = #{userId}
        """)
List<Role> selectRoleCodeByUserId(@Param("userId") Long userId);
第二步:在PermissionMapper.java接口中添加如下根据Role的id获取该Role拥有的Permission的信息:
@Select("""
        SELECT tb_permission.`code`
        FROM tb_role_permission INNER JOIN tb_permission
           ON tb_role_permission.permission_id = tb_permission.id
        WHERE
           tb_role_permission.role_id = #{roleId,jdbcType=BIGINT}
        """)
List<String> selectPermissionCodeByRoleId(@Param("roleId") Long roleId);
第三步:在UserServiceImpl类中添加如下根据用户id获取该用户拥有的角色与权限的方法:
@Resource
private RoleMapper roleMapper;
@Resource
private PermissionMapper permissionMapper;

@Override
public Set<String> getRolePermissionCodeByUserId(Long userId){
    Set<String> rolePermissionSet = new HashSet<>();
    根据用户id查询角色code
    List<Role> roleList = roleMapper.selectRoleCodeByUserId(userId);
    for (Role role : roleList) {
        rolePermissionSet.add("ROLE_"+role.getCode());
        // 根据角色id查询权限code
        List<String> permissionList = permissionMapper.selectPermissionCodeByRoleId(role.getId());
        for (String permission : permissionList) {
            rolePermissionSet.add(permission);
        }
    }

    return rolePermissionSet;
}

具体实现

第一步:修改UserDetails类,在其中添加如下代码:
//用户角色+权限
private Set<String> list;

public WegoUserDetails(User user, Set<String> list) {
    this.user = user;
    this.list = list;
}

private List<SimpleGrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    if(authorities == null) {
        authorities = list.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }
    return authorities;
}
第二步:修改UserDetailsService类,在其中添加获取用户权限的信息:
@Service
public class WegoUserDetailsService implements UserDetailsService {

    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //认证:根据用户名去查询对应的用户信息
        //Wrappers.<User>lambdaQuery().eq(User::getUsername,username);
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        User user = userService.getOne(queryWrapper);
        //如果没有查询到用户抛出异常
        if (user == null) {
            throw GlobalException.builder().code(401).msg("用户名或密码不正确").build();
        }

        //TODO: 授权:查询用户的权限信息
        //List<String> set = List.of("add", "get");
        Set<String> set = userService.getRolePermissionCodeByUserId(user.getId());

        // 将用户信息+权限信息封装成UserDetails对象
        WegoUserDetails wegoUserDetails = new WegoUserDetails(user, set);
        return wegoUserDetails;
    }

}
第三步:修改JwtAuthenticationFilter,在其中添加获取用户角色+权限的功能:
//TODO: 权限信息
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//此处必须使用三个参数的构造方法,其中第三个参数是权限信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
第四步:测试
  • 第一步:开启相关配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 第二步:在方法上添加注解
@RestController
public class TestController {
    @PreAuthorize("hasAuthority('sys:user:view')")
    @GetMapping("/fun1")
    public Result get() {
        return ResultUtil.success("sys:user:view");
    }

    @PreAuthorize("hasAuthority('sys:user:list')")
    @GetMapping("/fun2")
    public Result add() {
        return ResultUtil.success("sys:user:list");
    }

    @PreAuthorize("hasRole('admin')")
    @GetMapping("/fun3")
    public Result del() {
        return ResultUtil.success("admin");
    }

}

设置SpringSecurity跨域

第一步:设置SpringBoot允许跨域

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                //设置允许跨域的路径
                .addMapping("/**")
                //设置允许路霸请求的域名
                .allowedOriginPatterns("*")
                //是否允许cookie
                .allowCredentials(true)
                //设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                //设置允许的header属性
                .allowedHeaders("*")
                //跨域允许时间
                .maxAge(3600);
    }
}

第二步:在SpringSecurity配置类的configre()方法中添加如下代码:

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

相关文章:

  • MySQL知识点总结_1
  • 深入理解Python生成器
  • SpringBoot+Vue项目校园商铺系统
  • “不学数学就去当厨子”,兰大校友入选全球竞赛最强10人,决赛最后几小时才想起做题...
  • Python基础_判断语句(if、elif、else)、if 嵌套、逻辑运算符(and、or、not )、随机数的处理
  • 【C语言】小游戏系列——扫雷(内含详细过程)
  • C++系列文章 —— 类和对象篇(上)(从入门到精通合集)
  • 7.5 文件系统
  • java计算机毕业设计伊伊物流公司的管理系统源码+数据库+系统+lw文档+部署
  • PCB设计笔记
  • 图卷积神经网络(GCN)
  • 【数据结构】八大排序
  • D*(Dynamic A*)路径规划算法
  • 16.12 - 基于数据流设计用例
  • 大数据工程师、数据挖掘师和数据分析师有啥区别
  • -------------------- 第二讲-------- 第一节------在此给出链表的基本操作
  • Apache Pulsar 2.1 重磅发布
  • canvas 高仿 Apple Watch 表盘
  • flutter的key在widget list的作用以及必要性
  • JavaScript 基础知识 - 入门篇(一)
  • js写一个简单的选项卡
  • tab.js分享及浏览器兼容性问题汇总
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • 第三十一到第三十三天:我是精明的小卖家(一)
  • 力扣(LeetCode)56
  • 马上搞懂 GeoJSON
  • 如何使用 JavaScript 解析 URL
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 微信小程序实战练习(仿五洲到家微信版)
  • ​MySQL主从复制一致性检测
  • #、%和$符号在OGNL表达式中经常出现
  • #pragma预处理命令
  • #使用清华镜像源 安装/更新 指定版本tensorflow
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (附源码)基于SSM多源异构数据关联技术构建智能校园-计算机毕设 64366
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (一)Mocha源码阅读: 项目结构及命令行启动
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (一)基于IDEA的JAVA基础1
  • (一)基于IDEA的JAVA基础10
  • (转)nsfocus-绿盟科技笔试题目
  • (转载)深入super,看Python如何解决钻石继承难题
  • ****** 二 ******、软设笔记【数据结构】-KMP算法、树、二叉树
  • ***详解账号泄露:全球约1亿用户已泄露
  • .net 生成二级域名
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .Net6使用WebSocket与前端进行通信
  • .Net7 环境安装配置
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • /使用匿名内部类来复写Handler当中的handlerMessage()方法
  • @PreAuthorize注解
  • @Transaction注解失效的几种场景(附有示例代码)
  • [ C++ ] 继承