自学security笔记

学习链接:https://www.bilibili.com/video/BV1mm4y1X7Hc/?p=7&spm_id_from=pageDriver&vd_source=4cb41c702c93d4d052ec8d19f316525f

一、 搭建springboot项目

1. 创建maven项目

2. 导入基本依赖

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <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>

3. 引入SpringSecurity

 3.1 引入SpringSecurity依赖
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>

引入依赖后就需要登录后才能访问接口,用户名是user,密码在控制台输出

4. 认证

4.1 登录校验流程

在这里插入图片描述

4.2 springSecurity完整流程

在这里插入图片描述

4.3 认证流程详解

在这里插入图片描述
在这里插入图片描述

二、自定义登录

1. 思路分析

登录
① 自定义登录接口
 调用providerManager的方法进行认证,认证通过就生成jwt并把用户信息存入redis
② 自定义UserDetail接口实现类
 在数据库查询用户信息
鉴权
①定义jwt认证过滤器
 获取token,解析token获取userid,根据userid在redis查询用户信息,存入SecurityContextHolder

2. 准备工作

2.1 导入相关依赖
<!--redis依赖-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
	<groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.33</version>
</dependency>
<!-- jwt依赖 -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
2.2 redis配置类
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings(value = {"unchecked", "rawTypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        FastjsonRedisSerializer serializer = new FastjsonRedisSerializer(Object.class);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}
2.3 序列化工具类
public class FastjsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

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

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

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

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

    protected JavaType getJavaType(Class<?> clazz) {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}
2.4 jwt工具类
public class JwtUtils {

    /**
     * 两个常量: 过期时间;秘钥
     */
    public static final long EXPIRE = 1000*60*60*24;
    public static final String SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 生成token字符串的方法
     * @param id
     * @return
     */
    public static String getJwtToken(String id){
        String JwtToken = Jwts.builder()
                    //JWT头信息
                    .setHeaderParam("typ", "JWT")
                    .setHeaderParam("alg", "HS2256")
                    //设置分类;设置过期时间 一个当前时间,一个加上设置的过期时间常量
                    .setSubject("lin-user")
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                    //设置token主体信息,存储用户信息
                    .claim("id", id)
                    //.signWith(SignatureAlgorithm.ES256, SECRET)
                    .signWith(SignatureAlgorithm.HS256, SECRET)
                    .compact();
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @Param jwtToken
     */
    public static boolean checkToken(String jwtToken){
        if (StringUtils.isEmpty(jwtToken)){
            return false;
        }
        try{
            //验证token
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwtToken);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @Param request
     */
    public static boolean checkToken(HttpServletRequest request){
        try {
            String token = request.getHeader("token");
            if (StringUtils.isEmpty(token)){
                return false;
            }
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @Param token
     */
    public static String getMemberIdByJwtToken(String token){
        if (StringUtils.isEmpty(token)){
            return "";
        }
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        return (String) body.get("id");
    }
}
2.5 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -1L;
    private Long id;
    private String userName;
    private String nickName;
    private String password;
    // 0:正常 1:停用
    private int status;
    private String email;
    private String phonenumber;
    // 0:男 1:女 2:未知
    private int sex;

    public User(Long id, String userName, String password) {
        this.id = id;
        this.userName = userName;
        this.password = password;
    }
}

3.实现

3.1 环境准备
3.1.1 建表

在这里插入图片描述

3.1.2 引入数据库相关依赖

在这里插入图片描述

3.1.3 添加数据库配置
spring: 
  datasource: 
    url: 
    username: 
    password: 
    driver-class-name:
3.1.4 数据层

在这里插入图片描述

3.2 创建UserDetailService接口的实现类

实现UserDetailService接口,重写loadUserByUsername方法
该方法需要返回UserDetails类型,创建一个UserDetails接口的实现类,并重写相关方法

@Data
public class LoginUser implements UserDetails {
	// 用户信息
    private User user;

	// 权限列表
    private List<String> permissions;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(User user) {
        this.user = user;
    }

    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

	// 获取用户权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities == null) {
            authorities = permissions.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        return authorities;
    }

	// 获取用户账户
    @Override
    public String getPassword() {
        return user.getPassword();
    }

	// 获取用户密码
    @Override
    public String getUsername() {
        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;
    }
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    	//todo 这里应该查询数据库,简单测试直接返回一个固定对象
    	// {noop} 表示密码是明文
        return new LoginUser(new User(1L, "zs", "{noop}1234"), Arrays.asList("test"));
    }
}

在这里插入图片描述

3.3 密码加密

① 默认使用的passwordEncoder需要数据库中密码的格式为{id}password,会根据id判断加密方式,一般不采用这种方式。所以就需要替换PasswordEncoder
② 一般使用SpringSecurity提供的BCryptPasswordEncoder,直接注入使用
相同密码每次加密后都不一样,使用matches方法比较

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
3.4 自定义登录接口
3.4.1 controller
@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;

    @GetMapping("/user/login")
    public String login(@RequestParam("userName") String userName,
                      @RequestParam("password") String password) {
        return loginService.login(userName, password);
    }
}
3.4.2 service

在SecurityConfig中添加代码

	@Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

注入上面的bean,进行认证

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Override
    public String login(String userName, String password) {
    	// authenticationManager.authenticate进行用户认证
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
        Authentication authenticate = authenticationManager.authenticate(token);

		// 认证没通过,给出对应提示
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("登录失败");
        }
        
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        //todo 将用户信息存入redis
        String jwtToken = JwtUtils.getJwtToken(loginUser.getUser().getId().toString());
        System.out.println(JwtUtils.getMemberIdByJwtToken(jwtToken));
        // 登陆成功,返回jwt
        return jwtToken;
    }
}
3.4.3 放行登录接口

SecurityConfig类中重写configure方法

@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();
    }
3.5 认证
3.5.1 创建认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    	// 获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
        	// 放行,后面过滤器会抛出异常的
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            // 返回,避免
            return;
        }
        // 解析token
        String id = JwtUtils.getMemberIdByJwtToken(token);
        //todo 根据id从redis取
        LoginUser loginUser = new LoginUser(new User(Long.parseLong(id), "zs", "{noop}1234"), Arrays.asList("test"));
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        
		// 存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
3.5.2 将认证过滤器添加到UsernamePasswordAuthenticationFilter之前

① 注入过滤器

	@Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

② 在SecurityConfig的configure方法中添加代码

	http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

三、退出登录

实现

删除redis中的用户信息就行

	@Override
    public void logout() {
    	// 获取认证信息
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long id = loginUser.getUser().getId();
        System.out.println(id);
        //todo 根据id删除redis用户信息
    }

四、 鉴权

1. 设置访问权限

1.1 SecurityConfig类上添加@EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
1.2 在需要鉴权的接口上添加@PreAuthorize注解
	// 表示访问此接口需要read权限
    @PreAuthorize("hasAuthority('read')")
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

	// 表示访问此接口需要角色是system::admin
	@PreAuthorize("hasRole('system::admin')")
	@GetMapping("/hello1")
    public String hello1() {
        return "hello1";
    }

2. 封装权限信息

2.1 查询权限信息
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    	// 第二个参数是权限列表,根据用户名将查询出的权限放入
        return new LoginUser(new User(1L, "zs", "{noop}1234"), Arrays.asList("test"));
    }
}
2.2 UserDetails是使用getAuthorities方法获取权限,所以LoginUser需要重写getAuthorities方法
	// 定义成成员变量,避免每次调用方法都转换
	// 只有authorities为空时才转换
	// 注解作用:不序列化当前变量
	@JSONField(serialize = false)
    private List<GrantedAuthority> authorities;
    
	@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    	// 将permission中的String类型的权限转换为GrantedAuthority类型
        if (authorities == null) {
            authorities = permissions.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        return authorities;
    }

3. 校验权限信息

在JwtAuthenticationTokenFilter 的doFilterInternal方法中将用户的权限信息封装到UsernamePasswordAuthenticationToken中

	// 在redis中查询用户信息loginUser
	// 第三个参数loginUser.getAuthorities()就是用户的权限信息
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值