其他分享
首页 > 其他分享> > security入门

security入门

作者:互联网

整合Security

依赖

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

简单测试

@RequestMapping("/hello")
    public String hello(){
        return "hello";
    }

浏览器简单访问,用户名为Security默认的 user ,密码为控制台打印出来的一串

Using generated security password: 5eb2c001-423f-4c34-8518-2b20b4f6e1a8

{noop}

未加入密码加密解密时,数据库密码前加 '{noop}',表示明文存储,可跳过密码加密解密校验;

实现UserDetailsService

@Configuration
@RequiredArgsConstructor
public class MyUserDetails implements UserDetailsService {
	// 用于数据库交互
    private final SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        query.lambda().eq(SysUser::getUserName, username);
        SysUser sysUser = sysUserMapper.selectOne(query);
        if(sysUser == null){
            // 封装个我们自定义的用户对象,数据库表的扩展对象,可以用于鉴权信息
            System.out.println("handsome");
            throw new RuntimeException("用户名或者密码错误!");
        }
        // 用户存在,把数据封装成UserDetails返回
        // LoginUser实现UserDetails
        return new LoginUser().setSysUser(sysUser);
    }
}

实现UserDetails

@Getter
@Setter
@Accessors(chain = true)
public class LoginUser implements UserDetails {
	// 数据库对象
    private SysUser sysUser;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回权限信息
        return null;
    }

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

    @Override
    public String getUsername() {
        return sysUser.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;
    }
}

密码加密解密规则变更

默认使用PasswordEncoder有格式要求;

我们使用security就用security的加密解密规则:BCryptPasswordEncoder

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

}
// PasswordEncoder.matchs("明文密码","数据库加密密码") 返回true表示校验通过
new BCryptPasswordEncoder().encode("123")
变更下数据库密码

登录接口

1、security的配置登陆白名单

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭csrf(前后端分离、session没用)
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 白名单接口
                .antMatchers("/user/login").anonymous()
                // 除上面路径其余都要进行白名单校验
                .anyRequest().authenticated();

        http
                // 将我们的jwt过滤器添加到指定的过滤器之前,这样才能使过滤器生效
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }

2、校验用户账号密码,并将信息存储起来

@Override
    public MyResultCode login(SysUser sysUser) {
        // 用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName() ,sysUser.getPassword());
        // authenticate方法会调用我们重写的userDetails方法校验
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登陆失败,权限校验(账号密码)失败!");
        }
        // 生成token
        LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
        String userId = loginUser.getSysUser().getId().toString();
        String token = JwtTokenUtil.createJWT(loginUser.getSysUser().getId().toString(), userId, 60_000_000L);
        String myToken = "Bearer " + token;

        Map<String, Object> map = new HashMap<>(2);
        map.put("token", myToken);
        // 用户信息存在redis中,省得每次都从数据库拿,但是记得如果更改用户信息,需要刷新redis
        map.put("user", loginUser);

        // 放缓存里
        redis.set("user:token:" + userId, map);

        return MyResultCode.returnSuccess("登陆成功", map);

    }

3、jwt过滤器(解析token,并将token存入security上下文中)

package com.yuge.jwt.filter;

import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yuge.jwt.domian.LoginUser;
import com.yuge.jwt.util.JwtTokenUtil;
import com.yuge.jwt.util.RedisUtils;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
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.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

/**
 * <p>
 *    JWT过滤器
 *    OncePerRequestFilter spring提供的过滤器实现类(请求只会走一次 once)
 * </p>
 *
 * @author xy
 */
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final RedisUtils redis;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("Authorization");
        if(!StringUtils.isNotBlank(token)){
            // token不存在那就放行,因为后面的Security的过滤器会拦截他的
            filterChain.doFilter(request, response);
            // token都没了,也就没必要解析后面的token代码了
            return;
        }
        // 解析token
        LoginUser loginUser = new LoginUser();
        try {
            Claims claims = JwtTokenUtil.parseJWT(token.replaceAll("Bearer ", ""));
            String userId = claims.getSubject();
            Map<String, Object> map = (Map)redis.get("user:token:" + userId);
            Object o = map.get("user");
            JSONObject object = JSONUtil.parseObj(o);
            loginUser = JSONUtil.toBean(object, LoginUser.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 获取用户信息

        if(Objects.nonNull(loginUser)){
            // 用户信息存入SecurityContextHolder,因为后面的filter全部从这里拿取用户信息,判断是否有权限
            // 用户认证
            // TODO 获取权限信息封装到我们的 Authentication
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser, null, null);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        // 放行
        filterChain.doFilter(request, response);
    }
}


4、SecurityConfig配置使用Security的Authentication

/**
     * bean 注入 使用security的AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

5、将过滤器放入指定过滤器之前,使得之后的全部security过滤器都使用该鉴权方式

SecurityConfig文件中进行配置

http
                // 将我们的jwt过滤器添加到指定的过滤器之前,这样才能使过滤器生效
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

6、调用hello接口测试

退出登录

获取到用户的信息,从redis中删除,使其无法登录就好

@Override
    public MyResultCode loginOut() {
        // 去Security上下文中获取当前用户信息(也可以让前端直接传个用户id过来,反正找得到redis-key就行)
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser user = (LoginUser)authentication.getPrincipal();
        String userId = String.valueOf(user.getSysUser().getId());
        // redis中删除
        redis.del("user:token:" + userId);
        return MyResultCode.returnSuccess("退出成功", null);
    }

权限配置

开启权限,SecurityConfig类上添加注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

对应的方法上添加注解

// test为权限标识
@PreAuthorize("hasAuthority('test')")

登录时候获取权限

@Configuration
@RequiredArgsConstructor
public class MyUserDetails implements UserDetailsService {

    private final SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser> query = new QueryWrapper<>();
        query.lambda().eq(SysUser::getUserName, username);
        SysUser sysUser = sysUserMapper.selectOne(query);
        if(Objects.isNull(sysUser)){
            // 封装个我们自定义的用户对象,数据库表的扩展对象,可以用于鉴权信息
            throw new RuntimeException("用户名或者密码错误!");
        }
        // TODO 获取用户权限信息setPermissions 
        // 用户存在,把数据封装成UserDetails返回
        return new LoginUser().setSysUser(sysUser).setPermissions(Arrays.asList("hello","test"));
    }
}

调用其他接口时,进行的权限校验,是从UserDetails的getAuthorities()方法,

因此需要重写这个方法

private List<String> permissions;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回权限信息
        List<GrantedAuthority> collect = permissions.stream().map(e -> {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(e);
            return simpleGrantedAuthority;
        }).collect(Collectors.toList());
        return collect;
    }

在token校验过滤器中,将权限信息一起放入Security上下文中

if(Objects.nonNull(loginUser)){
            // 用户信息存入SecurityContextHolder,因为后面的filter全部从这里拿取用户信息,判断是否有权限
            // 用户认证
            // TODO 获取权限信息封装到我们的 Authentication
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

RBAC 用户角色权限关联表

Security自定义异常处理类

/**
 * <p>
 *     Security 权限不足异常
 * </p>
 *
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        MyResultCode returnMsg = new MyResultCode().setMsg("权限不足").setCode(HttpStatus.HTTP_FORBIDDEN).setData(null);
        String s = JSONUtil.toJsonStr(returnMsg);
        MyResultCode.responseJson(response, s);
    }
}
/**
 * <p>
 *     Security 身份认证异常
 * </p>
 *
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        MyResultCode returnMsg = new MyResultCode().setMsg("身份认证失败").setCode(HttpStatus.HTTP_UNAUTHORIZED).setData(null);
        String s = JSONUtil.toJsonStr(returnMsg);
        MyResultCode.responseJson(response, s);
    }
}
/**
     * Response输出Json格式
     *
     * @param response
     * @param data     返回数据
     */
    public static <T> void responseJson(HttpServletResponse response, String data) {
        response.setStatus(HttpStatus.HTTP_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        try {
            response.getWriter().print(data);
        } catch (IOException e) {

        }
    }

配置类更改

http.exceptionHandling()
                // 配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                // 配置权限不足处理器
                .accessDeniedHandler(accessDeniedHandler);

此外还可以配置登陆成功处理类、登出成功处理类等很多;自行百度;

跨域

跨域:浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域;

即:协议、端口、域名都要一样;

首先项目要开启跨域:springboot开启跨域,百度一下很简单;

security开启跨域,配置文件

http
                // 开启跨域
                .cors();

多种权限校验方式

也可以写在 securityconfig配置中

http.antMatchers.au***

// 指定权限
@PreAuthorize("hasAuthority('test')")
// 满足任意一个即有权限
@PreAuthorize("hasAnyAuthority('test','sf','sdf')")
// 需要拼接一个默认前缀 ROLE
@PreAuthorize("hasRole('test')")
// 同上拼接前缀,支持多个
@PreAuthorize("hasAnyRole('test')")

自定义权限校验

// @ 符号表示去找 spring bean    
@GetMapping("/hello1")
    @PreAuthorize("@yuge.hasAuthority('test')")
    public String hello1(){
        return "hello";
    }
/**
 * <p>
 *     自定义的权限校验
 * </p>
 *
 */
// 注入spring容器中
@Component("yuge")
public class CheckAuthorityImpl {
	// 仿照security的 方法校验,返回boolean类型就行
    public final boolean hasAuthority(String authority) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser principal = (LoginUser)authentication.getPrincipal();
        if(principal.getPermissions().contains(authority)){
            return true;
        }
        return false;
    }

}

认证成功处理器(省去登录接口)

本该写在登陆成功接口里面的方法直接可以写在我们自定义的处理类里面

// 配置登录URL
http.formLogin().loginProcessingUrl("/paas/v1/web/login/submit")
                // 配置登录成功处理类
                .successHandler(userLoginSuccessHandler)
/**
 * @description:登录成功处理类
 */
@Component
@Slf4j
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) {
        log.info("======登录成功======");

        SxUserDetails sxUserDetails = (SxUserDetails) authentication.getPrincipal();
        // 获得请求IP
        String ip = AccessAddressUtil.getIpAddress(request);
        sxUserDetails.setIp(ip);
        String token = JwtTokenUtil.createAccessToken(sxUserDetails);

        // 保存Token信息到Redis中
        JwtTokenUtil.setTokenInfo(token, sxUserDetails.getUsername(), ip);
        log.info("用户{}登录成功,Token信息已保存到Redis", sxUserDetails.getUsername());

        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        
        ResultData.responseJson(response, ResultData.response(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMsg(), tokenMap));
    }
}

扩展

security结合验证码

标签:return,入门,token,new,import,security,public,String
来源: https://www.cnblogs.com/xy20211005/p/16482877.html