SpringBoot之SpringSecurity框架

快速入门

引入依赖

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

引入依赖后访问接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。|

SpringSecurity拦截器

通过debug在计算器中使用run.getBean(DefaultSecurityFilterChain.class)查看
在这里插入图片描述
1)UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。
2)ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
3)FilterSecurityInterceptor:负责权限校验的过滤器。

认证

SpringSecurity完整流程:

在这里插入图片描述

认证原理流程图详解:

在这里插入图片描述

数据库校验用户核心代码实现

思路分析:

登录:
1)自定义登录接口
    调用ProviderManager的方法进行认证,如果认证通过生成jwt
    把用户信息存入redis中
2)自定义UserDetailsservice
    在这个实现列中去查询数据库
校验:
1)定义Jwt认证过滤器
    获取token
    解析token获取其中的userid
    从redis中获取用户信息
    存入SecurityContextHolder

准备工作

1、引入依赖

		<!--security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--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、添加相关配置
①RedisConfig

@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);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        //Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}

②RedisCache

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {

    @Autowired
    public RedisTemplate redisTemplate;

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

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

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }


    /**
     * has判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(final String key, final String hKey) {
        return redisTemplate.opsForHash().hasKey(key, hKey);
    }


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

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

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

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hKey
     */
    public void delCacheMapValue(final String key, final String hKey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

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


    /**
     * 删除Hash中的某条数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey) {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

}


③JwtUtil

public class JwtUtil {
    public static void main(String[] args) throws Exception {
        //生成jwtToken
        Map<String,Object> map = new HashMap<>();
        map.put("userId","123456");
        map.put("date",new Date());
        String subject = "java";
        String jwtToken = createJWT(subject, map);
        System.out.println(jwtToken);
        
        //解密
        Claims claims = parseJWT(jwtToken);
        System.out.println(claims);
        System.out.println(claims.getSubject());
    }

    //有效期设置为一小时
    public static final Long JWT_TTL = 60 * 60 * 1000L;
    //设置秘钥
    public static final String JWT_SECRET_KEY = "my";
    //设置加密算法
    public static final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    public static String createJWT(String subject, Map<String, Object> claims) {
        JwtBuilder builder = getJwtBuilder(subject, null, null, claims);
        return builder.compact();
    }

    public static String createJWT(String subject, Long ttlMillis, Map<String, Object> claims) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, null, claims);
        return builder.compact();
    }

    public static String createJWT(String subject, Long ttlMillis, String id, Map<String, Object> claims) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id, claims);
        return builder.compact();
    }

    /**
     * 生成jwtToken
     * @param: subject      设置sub:代表这个jwt所面向的用户
     * @param: ttlMillis    token超时时间
     * @param: uuid
     * @param: claims       指还想要在jwt中存储的一些非隐私信息
     * @return: io.jsonwebtoken.JwtBuilder
     **/
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid, Map<String, Object> claims) {
        SecretKey secretKey = generalSecretKey();
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();
        if(ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMi111s = nowMillis + ttlMillis;
        Date now = new Date (nowMillis);
        Date expDate = new Date(expMi111s);
        return Jwts.builder()
                .setClaims(claims)//指还想要在jwt中存储的一些非隐私信息
                .setId(uuid != null ? uuid : UUID.randomUUID().toString().replace("-", ""))//唯一的ID
                .setIssuer("faker")//签发者
                .setIssuedAt(now)//签发时问
                .setExpiration(expDate)//过期时间
                .setSubject(subject)//设置sub:代表这个jwt所面向的用户
                .signWith(signatureAlgorithm, secretKey);//设置签名:通过签名算法和秘钥生成签名
    }

    /**
     * 生成加密后的密钥 secretKey
     * @return
     **/
    private static SecretKey generalSecretKey() {
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_SECRET_KEY);
        SecretKey secretKey = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
        return secretKey;
    }

    /**
     * 解析
     * @param: jwtToken
     * @return:
     **/
    private static Claims parseJWT(String jwtToken) throws Exception{
        SecretKey secretKey = generalSecretKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判断jwtToken是否合法
//    public boolean isVerify(String jwtToken) {
//
//        //这个是官方的校验规则,这里只写了一个”校验算法“,可以自己加
//        Algorithm algorithm = null;
//        switch (signatureAlgorithm) {
//            case HS256:
//                algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
//                break;
//            default:
//                throw new RuntimeException("not support this algorithm");
//        }
//        JWTVerifier verifier = JWT.require(algorithm).build();
//        try {
//            verifier.verify(jwtToken);  // 校验不通过会抛出异常
//            //判断合法的标准:1. 头部和荷载部分没有篡改过。2. 没有过期
//        }catch (Exception e){
//            return false;
//        }
//        return true;
//    }
}

补充:JWT知识具体了解可参考:
https://blog.csdn.net/qq_41286145/article/details/120726638?spm=1001.2014.3001.5502
④WebUtils

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     * @param: response  渲染对象
     * @param: string  待渲染的字符串
     * @return: null
     **/
    public static String renderString(HttpServletResponse response, String string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

⑤FastJsonRedisSerializer

public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Class<T> c1azz;

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

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

    @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, c1azz);
    }

    protected JavaType getJavaType(Class<?> clazz) {
        return TypeFactory.defaultInstance().constructType(c1azz);
    }
}

⑥ResponseResult

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {

    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {

        this.code = code;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

⑦SecurityUser

@NoArgsConstructor
@AllArgsConstructor
@Data
@TableName(value = "sys_user")
public class SecurityUser implements Serializable {

    private static final long serialVersionUID = 1678985021436915824L;
    /**
     * 主键
     */
    @TableId
    private Long id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 密码
     */
    private String password;
    /**
     * 账号状态(0正常 1停用)
     */
    private String status;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机号
     */
    private String phoneNumber;
    /**
     * 用户性别(0男,1女,2未知)
     */
    private String sex;
    /**
     * 头像
     */
    private String avatar;
    /**
     * 用户类型(0管理员,1普通用户)
     */
    private String userType;
    /**
     * 创建人的用户id
     */
    private Long createBy;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新人
     */
    private Long updateBy;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 删除标志(0代表未删除,1代表已删除)
     */
    private Integer delFlag;
}

3、数据库建表

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '账号状态(0正常1停用)',
  `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `phone_number` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
  `user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
  `create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `del_flag` int(11) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;

代码具体实现

创建SecurityUserDao类实现BaseMapper接口

@Mapper
public interface SecurityUserDao extends BaseMapper<SecurityUser> {
}

创建UserDetailsServiceImpl 类实现UserDetailsService接口,重写其中的方法。根据用户名从数据库中查询用户信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SecurityUserDao securityUserDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<SecurityUser> lambda = new LambdaQueryWrapper();
        lambda.eq(SecurityUser::getUsername, username);
        SecurityUser securityUser = securityUserDao.selectOne(lambda);
        if(securityUser == null) {
            throw new RuntimeException("该用户不存在");
        }
        return new MyUserDetails(securityUser);
    }
}

因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类MyUserDetails ,实现该接口,把用户信息封装在其中

@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyUserDetails implements UserDetails {
    private SecurityUser securityUser;

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

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

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

在数据库增加一条数据,运行启动类,可以通过增加的用户进入页面
注:如果密码是明文存储,需要在密码前加{noop}
在这里插入图片描述
这样就可以通过账户admin,密码admin进入页面了


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

    我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
创建SecurityConfig类继承WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

测试验证

	//方式一:
	//@Autowired
    //private PasswordEncoder passwordEncoder;
    @Test
    public void test3() {
    	//方式二:
    	BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    	
        //客户端输入的密码
        String client = "admin";
        //服务端admin的加密密码
        String server = passwordEncoder.encode("admin");
        System.out.println(server);//$2a$10$vPc9fKFQ7GxMa.7o0odKseXyWi3ETCskuxEDRPU48RGE/bXA7fERG

        System.out.println(passwordEncoder.matches(client, server));//true
    }

登录接口

自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

@Controller
public class SecurityUserController {

    @Autowired
    private SecurityUserService securityUserService;

    @ResponseBody
    @PostMapping("/loginSecurity")
    public ResponseResult loginSecurity(@RequestBody SecurityUser securityUser) {
        return securityUserService.loginSecurity(securityUser);
    }
}

在业务层需要通过AuthenticationManager的authenticate()方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。因此需要继承WebSecurityConfigurerAdapter类的authenticationManagerBean()方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//前后端分离项目需要关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取Securitycontext
                .and()
                .authorizeRequests()
                .antMatchers ("/loginSecurity").anonymous ()//对于登录接口允许匿名防问
                .anyRequest().authenticated();//除上面外的所有请求全部需要鉴权认证
    }
}

重要:把用户名和密码封装成Authentication的实现类UsernamePasswordAuthenticationToken 对象,然后把这个对象传入到authenticate()方法中,让它帮我们进行一个认证操作,这个方法会调用通过实现UserDetailsService的子类的loadUserByUsername()方法进行一个用户的校验,去查询数据有没有这个用户,如果查到了的话就把securityUser对象数据封装到自定义实现UserDetails的子类MyUserDetails当中,然后返回一个myUserDetails对象,最后返回给authentication对象,通过调用它的getPrincipal()方法强转成myUserDetails对象获取它的数据,最后通过工具类JwtUtil的createJWT()方法生成一个jwtToken并返回给客户端。

@Service
public class SecurityUserServiceImpl implements SecurityUserService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult loginSecurity(SecurityUser securityUser) {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(securityUser.getUsername(), securityUser.getPassword());
        Authentication authenticate = authenticationManager.authenticate(token);
        if(authenticate == null) {
            throw new RuntimeException("用户名或密码错误");
        }
        MyUserDetails principal = (MyUserDetails) authenticate.getPrincipal();
        SecurityUser user = principal.getSecurityUser();
        String jwtToken = JwtUtil.createJWT(user.getId().toString(), null);
        Map<String, Object> map = new HashMap<>();
        map.put("token", jwtToken);

        //把完整的用户信息存入redis userid作为key
        redisCache.setCacheObject("login:" + user.getId().toString(), principal);
        return new ResponseResult(200, "登录成功", map);
    }
}

认证过滤器

自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userId。使用userId去redis中获取对应的SecurityUser对象,然后封装Authentication对象存入SecurityContextHolder。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        String userId = null;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        MyUserDetails myUserDetails = redisCache.getCacheObject("login:" + userId);
        if(myUserDetails == null) {
            throw new RuntimeException("用户未登录");
        }
        //三个参数的构造器中有个super.setAuthenticated(true)代表已认证的状态
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(myUserDetails, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

在WebSecurityConfigurerAdapter的实现配置类SecurityConfi的configure()方法中配置自定义过滤器

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//前后端分离项目需要关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取Securitycontext
                .and()
                .authorizeRequests()
                .antMatchers("/druid/**", "/getCount").permitAll()//不管登录没登录都允许访问
                .antMatchers ( "/loginSecurity").anonymous ()//对于登录接口允许匿名防问
                .anyRequest().authenticated()//除上面外的所有请求全部需要鉴权认证
                .and()
                //在UsernamePasswordAuthenticationFilter拦截器之前先进行拦截
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

退出登录

SecurityUserController类新增接口

	@ResponseBody
    @RequestMapping(value = "/logoutSecurity",method = {RequestMethod.GET})
    public ResponseResult logoutSecurity() {
        return securityUserService.logoutSecurity();
    }

SecurityUserServiceImpl类实现该方法

	@Override
    public ResponseResult logoutSecurity() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails principal = (MyUserDetails)authentication.getPrincipal();
        SecurityUser securityUser = principal.getSecurityUser();
        redisCache.deleteObject("login:" + securityUser.getId().toString());
        return new ResponseResult(200, "注销成功");
    }

授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限,只需要把当前登录用户的权限信息也存入tuthentication,然后设置所需要的权限即可。

从数据库查询权限信息

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
在这里插入图片描述
数据库建表

DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu`(
	`id` bigint(20) NOT NULL AUTO_INCREMENT,
	`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
	`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
	`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
	`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示1隐藏)',
	`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常1停用)',
	`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
	`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
	`create_by` bigint(20) DEFAULT NULL,
	`create_time` datetime DEFAULT NULL,
	`update_by` bigint(20) DEFAULT NULL,
	`update_time` datetime DEFAULT NULL,
	`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除1已删除)',
	`remark` varchar(500) DEFAULT NULL COMMENT '备注',
	PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role`(
	`id` bigint(20) NOT NULL AUTO_INCREMENT,
	`name` varchar(128) DEFAULT NULL,
	`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
	`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
	`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
	`create_by` bigint(200) DEFAULT NULL,
	`create_time` datetime DEFAULT NULL,
	`update_by` bigint(200) DEFAULT NULL,
	`update_time` datetime DEFAULT NULL,
	`remark` varchar(500) DEFAULT NULL COMMENT '备注',
	PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

DROP TABLE IF EXISTS `sys_role_menu`;

CREATE TABLE `sys_role_menu`(
	`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
	`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单ID',
	PRIMARY KEY (`role_id`, `menu_id`)
)ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role`(
	`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
	`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
	PRIMARY KEY (`user_id`, `role_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

通过用户id查询对应的权限信息

SELECT 
	DISTINCT m.perms
FROM 
	sys_user_role ur
	LEFT JOIN sys_role r ON ur.role_id = r.id
	LEFT JOIN sys_role_menu rm ON r.id = rm.role_id
	LEFT JOIN sys_menu m ON rm.menu_id = m.id
WHERE 
	user_id = 1 and r.`status` = 0 and m.`status` = 0;

创建对应的权限实体表

@NoArgsConstructor
@AllArgsConstructor
@Data
@TableName(value = "sys_menu")
public class SecurityMenu implements Serializable {
    private static final long serialVersionUID = -1588371465115638351L;
    /**
     * 主键
     */
    @TableId
    private Long id;
    /**
     * 菜单名
     */
    private String menuName;
    /**
     * 路由地址
     */
    private String path;
    /**
     * 组件路径
     */
    private String component;
    /**
     * 菜单状态(0显示1隐藏)
     */
    private String visible;
    /**
     * 菜单状态(0正常1停用)
     */
    private String status;
    /**
     * 权限标识
     */
    private String perms;
    /**
     * 菜单图标
     */
    private String icon;
    /**
     * 创建人的用户id
     */
    private Long createBy;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新人
     */
    private Long updateBy;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 是否删除(0代表未删除,1代表已删除)
     */
    private Integer delFlag;
    /**
     * 备注
     */
    private String remark;
}

限制访问资源所需权限代码实现

SpringSecurity为两大类的权限控制方案,一种是基于注解形式的方案,一种是基于配置形式的方案。基于配置的方案主要用于配置静态资源,而在前后端分离的项目中,后端项目没有什么静态资源,因此一般使用注解去指定访问对应的资源所需的权限。
重要:配置权限前,一定要在配置类上先开启相关配置。

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

SecurityMenuDao 接口

@Mapper
public interface SecurityMenuDao extends BaseMapper<SecurityMenu> {
    List<String> selectPermsByUserId(Long id);
}

对应SecurityMenuMapper.xml文件

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.SecurityMenuDao">
    <select id="selectPermsByUserId" resultType="String">
        SELECT
			DISTINCT m.perms
		FROM
			sys_user_role ur
			LEFT JOIN sys_role r ON ur.role_id = r.id
			LEFT JOIN sys_role_menu rm ON r.id = rm.role_id
			LEFT JOIN sys_menu m ON rm.menu_id = m.id
		WHERE
			user_id = #{id} and r.`status` = 0 and m.`status` = 0;
    </select>
</mapper>

UserDetailsServiceImpl在查询出用户后还要获取对应的权限信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SecurityUserDao securityUserDao;
    @Autowired
    private SecurityMenuDao securityMenuDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<SecurityUser> lambda = new LambdaQueryWrapper();
        lambda.eq(SecurityUser::getUsername, username);
        SecurityUser securityUser = securityUserDao.selectOne(lambda);
        if(securityUser == null) {
            throw new RuntimeException("该用户不存在");
        }
        //在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
        List<String> permissions = securityMenuDao.selectPermsByUserId(securityUser.getId());
        return new MyUserDetails(securityUser, permissions);
    }
}

把权限信息封装到UserDetails的实现类中返回。

@NoArgsConstructor
@Data
public class MyUserDetails implements UserDetails {

    private SecurityUser securityUser;
    private List<String> permissions;

    public MyUserDetails(SecurityUser securityUser, List<String> permissions) {
        this.securityUser = securityUser;
        this.permissions = permissions;
    }

    @JSONField(serialize = false)
    //GrantedAuthority反序列化之后可能会有出题,所以需要把它进行忽略不进行序列化
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //框架内部每一次调用这个方法都需要进行集合的转换会比较耗时,因此直接设置为成员变量第一次进行转换就可以
        if(authorities != null) {
            return authorities;
        }
        authorities = new ArrayList<>();
        for (String permission : permissions) {
            //把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }
        return authorities;
    }

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

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

JwtAuthenticationTokenFilter拦截器中获取权限信息封装到Authentication中

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        String userId = null;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        MyUserDetails myUserDetails = redisCache.getCacheObject("login:" + userId);
        if(myUserDetails == null) {
            throw new RuntimeException("用户未登录");
        }

        //三个参数的构造器中有个super.setAuthenticated(true)代表已认证的状态
        //获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

方式一:通过框架@PreAuthorize("hasAuthority('admin')")注解在接口的方法上配置权限
方式二:创建MySecurityExpressionRoot类自定义权限校验,通过@PreAuthorize("@ex.hasAuthority('system:test:list')")注解在接口的方法上配置权限
方式三:在WebSecurityConfigurerAdapter接口的实现配置类SecurityConfig中加入以下一行配置
http.authorizeRequests().antMatchers(“/helloSecurity”).hasAuthority(“system:test:list”);

@Controller
public class SecurityUserController {

    @Autowired
    private SecurityUserService securityUserService;

    @ResponseBody
    @PostMapping("/loginSecurity")
//    @RequestMapping(value = "/loginSecurity",method = {RequestMethod.POST})
    public ResponseResult loginSecurity(@RequestBody SecurityUser securityUser) {
        return securityUserService.loginSecurity(securityUser);
    }

    @ResponseBody
    @RequestMapping(value = "/logoutSecurity",method = {RequestMethod.GET})
    public ResponseResult logoutSecurity() {
        return securityUserService.logoutSecurity();
    }

    //方式一:框架内部定义
    @ResponseBody
    @PreAuthorize("hasAuthority('system:test:list')")
    @RequestMapping(value = "/helloSecurity", method = RequestMethod.GET)
    public String helloSecurity(){
        return "helloSecurity";
    }

    //方式二:自定义权限校验
    @ResponseBody
    @PreAuthorize("@ex.hasAuthority('system:test:list')")
    @RequestMapping(value = "/helloExpression", method = RequestMethod.GET)
    public String helloExpression(){
        return "helloExpression";
    }
}

========================================================================================
@Component("ex")
public class MySecurityExpressionRoot {

    public boolean hasAuthority(String authority) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails principal = (MyUserDetails) authentication.getPrincipal();
        List<String> permissions = principal.getPermissions();
        return permissions.contains(authority);
    }
}

自定义异常处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
● 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
● 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
1、自定义AuthenticationEntryPoint的实现类AuthenticationEntryPointImpl通过@Component注解加入容器中

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}

2、自定义AuthenticationEntryPoint的实现类AccessDeniedHandlerImpl通过@Component注解加入容器中

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}

3、在配置类SecurityConfig中配置异常处理器

	@Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//前后端分离项目需要关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取Securitycontext
                .and()
                .authorizeRequests()
                .antMatchers("/druid/**", "/getCount").permitAll()//不管登录没登录都允许访问
                .antMatchers ( "/loginSecurity").anonymous ()//对于登录接口允许匿名防问
                .anyRequest().authenticated()//除上面外的所有请求全部需要鉴权认证
                .and()
                //在UsernamePasswordAuthenticationFilter拦截器之前先进行拦截
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling()//配置异常处理器
                .authenticationEntryPoint(authenticationEntryPoint)//配置认证失败处理器
                .accessDeniedHandler(accessDeniedHandler);//配置授权失败处理器
    }

跨域

    浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
    前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。所以我们就要处理一下,让前端能进行跨域请求。
1、先对SpringBoot配置,运行跨域请求

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

2、开启SpringSecurity的跨域访问
由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//前后端分离项目需要关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取Securitycontext
                .and()
                .authorizeRequests()
                .antMatchers("/druid/**", "/getCount").permitAll()//不管登录没登录都允许访问
                .antMatchers ( "/loginSecurity").anonymous ()//对于登录接口允许匿名防问
                .anyRequest().authenticated()//除上面外的所有请求全部需要鉴权认证
                .and()
                //在UsernamePasswordAuthenticationFilter拦截器之前先进行拦截
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling()//配置异常处理器
                .authenticationEntryPoint(authenticationEntryPoint)//配置认证失败处理器
                .accessDeniedHandler(accessDeniedHandler);//配置授权失败处理器
        http.cors();//允许跨域
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值