SpringSecurity +Jwt 使用用户名密码登录

一、pom.xml文件(关键依赖)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

二、application.yml

spring
  security:
  user:
    password: 1234
    name: user

三、SpringSecurity.config配置文件

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 开启注解权限管理
public class SecurityConfig {

    // 自定义jwt解析,将登录者的权限设置到SecurityContextHolder中,其他地方进行设置权限验证
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 认证
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    // 这里改变一下密码编码和比对的密码方式
    // 默认方式为将密码编码后,我们需要在密码加{noop}进行比对
    // String encode_pwd = passwordEncoder.encode("123456"); 这样便可以得到加密后的密码
    // 这里我们将它注入容器即可,默认使用当前版本+长度+随机数产生随机盐生成密码,官方推荐使用
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 返回AuthenticationManager对象,在认证的时候需要使用到此对象进行认证
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }
    // 定义SpringSecurity不需要拦截的url
    private static final String[] URL_WHITELISTS = {
            "/common/**",
            "/user/login",
            "/user/sendMsg",
            "/doc.html",
            "/webjars/**",
            "/swagger-resources",
            "/v2/api-docs"
    };

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 将跨站请求伪造防护关闭,我们使用jwt保证安全
        return http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and() // 下面三个有顺序要求
                // 拦截所有请求
                .authorizeRequests()
                // 放行一些请求,不需要认证
                .antMatchers(URL_WHITELISTS).permitAll()
                // 所有请求需要认证
                .anyRequest().authenticated()
                .and()
                // 配置自定义jwt解析过滤器,在UsernamePasswordAuthenticationFilter之前执行
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 配置异常
                .exceptionHandling()
                // 认证异常,可以自定义返回消息(可以不配)
                .authenticationEntryPoint(authenticationEntryPoint)
//                // 授权异常,可以自定义返回消息(可以不配)
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                // 开启跨域访问
                .cors()
                .and()
                .build();
    }

}

四、LoginController(登录控制)

1、将前端传入的用户名和密码封装成UsernamePasswordAuthenticationToken然后使用authenticationManager.authenticate(authenticationToken) 进行认证

    @Autowired // 认证管理器,在SpringSecurity中注入
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public R login(@RequestBody User user) {
        log.info("登录...");
        // 1、将用户输入的用户名和密码进行封装成Authentication
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        // 2、进行认证,最终会调用UserDetailsService.loadUserByUsername ,
// 在这里我们需要实现从数据库中查询数据,这里可以先看下面那个认证过程再回来
        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        // 3、authenticate 为空则是认证失败,不为空,里面存有员工信息
        if (Objects.isNull(authentication)) {
            throw new RuntimeException("登录失败");
        }
        // 4、获取用户信息,将用户id生成token,以 login: + userId 为键,用户信息 为值存入Redis缓存
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        redisCache.setCacheObject("login:" + userId, loginUser,3, TimeUnit.DAYS);
        // 5、返回给客户端JwtToken
        String jwt_token = JwtUtil.createJWT(userId);
        Map<String, Object> map = new HashMap<>();
        map.put("token", jwt_token);
        User em = loginUser.getUser();
        em.setPassword(null);
        map.put("userInfo", em);
        return R.success(map);
    }

2、调用UserDetailsService.loadUserByUsername,这里我们自定义一个类实现UserDetailsService,重写该方法,实现从数据库查询数据,默认是从内存中查询,要注意返回的是UserDetails对象,下面介绍

public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(wrapper);
        // 用户不存在,进行异常处理
        if (Objects.isNull(user)) {
            throw new RuntimeException("员工不存在");
        }
        // 查詢權限信息,存入UserDetails中
        // TODO  这里权限信息未查
        // 模拟角色数据,在LoginUser中将list转成权限对象
        List<String> list = Arrays.asList("ROLE_normal", "ROLE_root");
        // 返回UserDetails的实现类
        UserDetails userDetails = new LoginUser(user, list);
        return userDetails;
    }
}

3、UserDetails对象,这是一个接口,所以我们需要找实现类,常用的有与它同一目录下的User对象,这里定义扩展UserDetails的实现类LoginUser

package org.springframework.security.core.userdetails;
...
public interface UserDetails extends Serializable {
    // 权限
	// 用户的权限集, 默认需要添加ROLE_ 前缀
	Collection<? extends GrantedAuthority> getAuthorities();
    // 密码
	// 用户的加密后的密码, 不加密会使用{noop}前缀
	String getPassword();
    // 用户名
	String getUsername();
    // 获取帐户是否过期
	boolean isAccountNonExpired();
    // 获取帐户是否锁定
	boolean isAccountNonLocked();
    // 获取凭证是否过期
	boolean isCredentialsNonExpired();
    // 获取用户是否可用
	boolean isEnabled();

}

4、定义UserDetails的实现类LoginUser,作用便是多存储用户对象,与权限信息。在认证完成后便将LoginUser 存储到Redis,方便我们获取权限设置到SecurityContextHolder中,这个对象用户全局获取用户信息,在实现类,全部获取改为true

@Data
@NoArgsConstructor
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;
    }
    
    // 这里做了一些改变,传入list集合便可以封装成GrantedAuthority对象
    // 加入Redis时忽略此字段
    //@JSONField(serialize = false)
    private List<GrantedAuthority> grantedAuthorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (grantedAuthorities!=null){
            return grantedAuthorities;
        }
        grantedAuthorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return grantedAuthorities;
    }

    @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;}
}

回到authentication认证完成,返回Authentication对象,这里我们看到可以通过

(LoginUser) authentication.getPrincipal()获取UserDetails对象,user是我们从数据库中查询,permissions也是我们从数据库查询(这里定义方便演示),封装成GrantedAuthorities对象,是为了认证接口,它会从这个里面查询此用户有没有这里面的权限

 五、生成JwtToken,返回给前端

了解JwtToken可以看:https://blog.csdn.net/qq_45524787/article/details/125455522

这里涉及到的RedisCache、JwtUtils皆是工具类,后面各位大爷看一下便懂

 最后返回token与用户信息

 前端部分代码:

1、login.vue

methods: {
    // 登录
    async handleLogin() {
        this.$refs.loginForm.validate(async (valid) => {
            if (valid) {
                this.loading = true
                let res = await loginApi(this.loginForm)
                if (typeof res != "undefined" && String(res.code) === '1') {//1表示登录成功
                    // 这里将获取的token存入本地域中,也可以存入session域中
                    // 方便在请求拦截器中,设置请求携带token
                    localStorage.setItem('token', res.data.token)
                    localStorage.setItem('userInfo',JSON.stringify(res.data.userInfo))
                    this.$router.push('/')
                } else {
                    this.$message.error("用户名或密码错误,请重新登录")
                    this.loading = false
                }
            }
        })
    }
}

 2、request.js

service.interceptors.request.use((config) => {
    // 判断是否存在token,如果存在的话,则每个http header中都加上token
    if (window.localStorage.getItem('token')) {  
        config.headers.token = localStorage.getItem('token');
    }
}

 3、登录完成过后,发送新的请求,携带token,后端需要解析token(以userId生成的),获取userId

后端使用过滤器拦截每一个请求,解析token统一操作

4、自定义认证信息过滤,解析请求中的token信息,放在UsernamePasswordAuthenticationFilter前面,这里在SecurityConfig中配置了,继承只执行一次的filter

// 自定义认证信息过滤,解析请求中的token信息,放在UsernamePasswordAuthenticationFilter前面,继承只执行一次的filter
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    //路径匹配器,支持通配符(日志打印用的,不需要了解)
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 打印放行的请求(日志打印用的,不需要了解)
        greenLightRequest(request, response);

        // 从请求头中获取token信息
        String token = request.getHeader("token");
        log.info("token:{}", token);
        log.info("当前线程名称:{}", Thread.currentThread().getName());
        // 判断token是否为空
        if (!StringUtils.hasText(token)) {
            log.info("token为空");
            // 为空放行
            filterChain.doFilter(request, response);
            // 停止向下执行
            return;
        }
        String id;
        try {
            // 解析token
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        // 根据键从Redis中获取用户信息
        String redisKey = "login:" + id;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("登录失败");
        }
        // 判断用户信息是否存在,也许前端非法传入token,在Redis内查询不到用户信息
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(request, response);
    }

    /**
     * 路径匹配,检查本次请求是否需要放行(日志打印用的,不需要了解)
     *
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls, String requestURI) {
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match) {
                return true;
            }
        }
        return false;
    }

    // 打印不需要拦截的请求(日志打印用的,不需要了解)
    public void greenLightRequest(HttpServletRequest request, HttpServletResponse response) {
        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();// /backend/index.html
        log.info("拦截到请求:{}", requestURI);
        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/plugins/**",
                "/front/**",
                "/common/**",
                "/user/login",
                "/user/sendMsg"
        };
        //2、判断本次请求是否需要处理
        //3、如果不需要处理,打印,放行在后面执行
        if (check(urls, requestURI)) {
            log.info("本次请求{}不需要处理", requestURI);
        }
    }
}

解析JwtToken,看一下

获取Redis中存储的用户信息,权限信息

 

将权限信息设置到SecurityContextHolder中 ,在需要进行认证的接口上添加授权信息,下一次访问该接口时就需要携带对应的权限才能访问

 这个接口便需要ROLE_root权限才能访问。

补充:授权这里使用Spring-EL expression表达式,各位自行了解

六、RedisCache工具类

package com.ll.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@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 缓存键值
     * @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);
    }
}

JwtUtil工具类

package com.ll.utils;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "1q2w3e4r";

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

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("lw")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        String token = "xxx.yyy.zzz";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }

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

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}

 有条件的大爷,给个三连!!!

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值