SpringSecurity实现用户认证

jasmine 于 2020-05-03 发布

1、引入Spring Security依赖

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

2、添加配置类

import com.company.da.project.auth.JwtTokenFilter;
import com.company.da.project.auth.MyAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.time.Duration;
import java.util.Collections;


@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtTokenFilter jwtTokenFilter;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    /**
     * 对密码进行加密的类,配置在spring中,方便调用
     * 例如:bCryptPasswordEncoder.encode(registerUser.get("password"))
     * 是 PasswordEncoder实现类
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncode() {
        return new BCryptPasswordEncoder();
    }

    /**
     * loginService中验证usernamePasswordAuthenticationToken中用户名和密码
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * 重写configure方法,根据项目需求配置,并将将写好的相应配置类引入
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //关闭session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //授权
                .authorizeRequests()
                //对于登陆接口允许访问/login是不需要带项目路径/dac的
                .antMatchers("/login").anonymous()
                //其他接口要走验证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.cors().configurationSource(corsConfigurationSource());//允许跨域
        //增加授权异常处理
        http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint);
    }

    /**
     * 解决跨域问题
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true);
        configuration.setAllowedOrigins(Collections.singletonList("*"));
        configuration.setAllowedMethods(Collections.singletonList("*"));
        configuration.setAllowedHeaders(Collections.singletonList("*"));
        configuration.setMaxAge(Duration.ofHours(1));
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

过滤器,每次请求进行token验证

import com.company.da.project.common.RedisConstant;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.dto.RedisUser;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.RedisUtil;
import com.company.da.project.util.coding.Coding;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * 这个拦截器将redis中的用户信息获取到,放到context中,后面的filter看见上下问中有用户信息,说明是验证过的,直接放行
 */
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws ServletException, IOException {
        //1、获取token
        //2、解析token
        //3、获取用户信息,可以是redis或者数据库
        //4、将用户信息存入应用上下文中
        String userId = request.getHeader("token");
        String redisUserStr = (String) redisUtil.get(RedisConstant.ACS_TOKEN + userId);
        if (Coding.notNull(redisUserStr)) {
            RedisUser redisUser = JsonUtils.toBean(redisUserStr, RedisUser.class);
            if (Coding.notNull(redisUser)) {
                //将用户信息放进去
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(new JwtUser(redisUser), null, null);
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //应用上下文中设置登录用户信息,此时Authentication类型为User
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        //如果token为空,直接放行,后面会有拦截器处理
        chain.doFilter(request, response);
    }
}

实现UserDetailsService接口,对用户查询进行支持

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.company.da.project.common.exception.PlatformException;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.repository.entity.UserInfo;
import com.company.da.project.repository.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author: jasmine
 */
@Service
public class UserDetialServiceImpl implements UserDetailsService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws PlatformException {
        //查询用户信息
        List<UserInfo> userInfos = userInfoMapper.selectList(Wrappers.lambdaQuery(UserInfo.class).eq(UserInfo::getName, username));
        UserInfo userInfo = userInfos.stream().findFirst().orElse(null);
        if (null == userInfo) {
            throw new PlatformException("用户名或者密码错误");
        }
        //TODO 查询对应的权限信息
        //封装UserDetails对象
        return new JwtUser(userInfo);
    }
}

用户实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.company.da.project.util.MyUtil;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

/**
 * @author jasmine
 */
@Getter
@Setter
@NoArgsConstructor
@TableName(value = "da_user_info")
public class UserInfo {
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户唯一ID
     */
    private String userId;

    /**
     * 用户名
     */
    private String name;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 手机号
     */
    private String mobile;

    /**
     * 密码
     */
    private String password;

    /**
     * 出生日期
     */
    private String birthday;

    /**
     * 性别,0未知,1男,2女
     */
    private Integer sex;

    /**
     * 用户来源
     */
    private Integer sourceFrom;

    /**
     * 头像
     */
    private String avatar;

    /**
     * 状态
     */
    private Integer state;

    /**
     * 是否删除
     */
    private Integer isdel;

    /**
     * 更新时间
     */
    private LocalDateTime actionTime;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    public UserInfo(String userName, String encodePwd) {
        this.name = userName;
        this.password = encodePwd;
        this.userId = MyUtil.getUniqueId();
    }
}

JWTUser对象类,对UserDetails接口实现,支持框架中用户信息传递

import com.company.da.project.repository.entity.UserInfo;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @author: jasmine
 */
@Getter
@Setter
@NoArgsConstructor
public class JwtUser implements UserDetails {
    /**
     * 主键ID
     */
    private Long id;

    /**
     * 用户唯一ID
     */
    private String userId;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 权限信息
     */
    private Collection<? extends GrantedAuthority> authorities;

    /**
     * 无参构造器
     */
    public JwtUser(UserInfo userInfo) {
        this.username = userInfo.getName();
        this.userId = userInfo.getUserId();
        this.password = userInfo.getPassword();
        this.id = userInfo.getId();
    }

    public JwtUser(RedisUser redisUser) {
        this.username = redisUser.getUserName();
        this.userId = redisUser.getUserId();
        this.password = redisUser.getPassword();
        this.id = redisUser.getId();
    }

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

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

    @Override
    public String getUsername() {
        return this.username;
    }

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

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

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

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

处理认证失败异常,当有认证失败的时候,封装请求体为项目中Response标准格式

import com.company.da.project.common.Response;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.coding.Coding;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 未认证异常处理
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        Response<String> toLogin = Response.error(Coding.strOrEmpty(HttpStatus.UNAUTHORIZED.value()), "未认证,请登陆");
        String res = JsonUtils.toString(toLogin);
        response.setStatus(401);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        try {
            response.getWriter().write(res);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

业务接口Controller,定义登陆、退出、注册接口

import com.company.da.project.common.Response;
import com.company.da.project.controller.login.form.LoginUser;
import com.company.da.project.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: jasmine
 */
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public Response<String> login(@RequestBody LoginUser loginUser) {
        return Response.success(loginService.login(loginUser));
    }

    @GetMapping("/exit")
    public Response<String> logout() {
        return Response.success(loginService.logout());
    }

    @PostMapping("/zhuce")
    public Response<String> register(@RequestBody LoginUser loginUser) {
        return Response.success(loginService.register(loginUser));
    }
}

LoginService对登陆、退出、注册的逻辑实现

import com.company.da.project.common.RedisConstant;
import com.company.da.project.common.exception.PlatformException;
import com.company.da.project.controller.login.form.LoginUser;
import com.company.da.project.dto.JwtUser;
import com.company.da.project.dto.RedisUser;
import com.company.da.project.util.JsonUtils;
import com.company.da.project.util.RedisUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Calendar;
import java.util.Date;

/**
 * @author: jasmine
 */
@Service
public class LoginService {
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private UserService userService;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public String login(LoginUser loginUser) {
        String userName = loginUser.getUserName();
        String password = loginUser.getPassword();

        //1、authenticationManager对象验证usernamePasswordAuthenticationToken中用户名和密码
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, password);
        //底层调用UserDetialServiceImpl中的loadUserByUsername查询用户信息,并进行密码比对
        //如果用户名或者密码错误,会在这个函数中校验返回,不继续执行
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

        //登陆失败 TODO 走不到这个方法
        if (null == authenticate) {
            throw new PlatformException("登陆失败");
        }

        //2、如果登陆成功,获取JwtUser对象
        JwtUser jwtUser = (JwtUser) authenticate.getPrincipal();

        //3、生成jwt,并将jwt返回
        String userId = jwtUser.getUserId();

        // 生成token过程
        //String token = genToken(userId);

        //将完整user对象存入redis
        redisUtil.set(RedisConstant.ACS_TOKEN + userId, JsonUtils.toString(new RedisUser(jwtUser)), RedisConstant.ACS_TOKEN_TTL);

        return userId;
    }

    //生成token过程
    private String genToken(String userId) {
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        // 设置签发时间
        calendar.setTime(new Date());
        // 设置过期时间
        calendar.add(Calendar.MINUTE, 5);// 5分钟
        Date time = calendar.getTime();
        String token = Jwts.builder()
                .setSubject(userId)
                .setIssuedAt(now)//签发时间
                .setExpiration(time)//过期时间
                .signWith(SignatureAlgorithm.HS512, "spring-security-@Jwt!&Secret^#") //采用什么算法是可以自己选择的,不一定非要采用HS512
                .compact();
        return token;
    }

    public String logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        String userId = jwtUser.getUserId();
        boolean del = redisUtil.del(RedisConstant.ACS_TOKEN + userId);
        return del ? "success" : "fail";
    }

    public String register(LoginUser loginUser) {
        String userName = loginUser.getUserName();
        String encodePwd = bCryptPasswordEncoder.encode(loginUser.getPassword());
        return userService.updateOrInitUser(userName, encodePwd);
    }
}