紧接着上一篇,那我们开始写我们的登录功能吧~

1.登录功能

登录功能使用Spring Security安全框架和JWT令牌实现

整体流程:

首先是前端传用户名、密码和验证码给后端,后端会先去校验传过来的用户名和密码,如果用户名、密码或验证码有错误,那么我们就直接让用户重新输入;反之用户输入正确的数据,生成一个JWT令牌并且返回给前端。前端拿到JWT令牌之后就会放在请求头里面,后面的任何请求都会携带这个JWT令牌,后端也会有一个拦截器去对这个JWT做出相应的验证,验证通过之后才能访问到对应的接口,不通过就说明那么JWT令牌失效了,要么就是这个用户名或者密码有问题。当然还需要我们输入正确的验证码。

1.1.导入依赖

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

1.2.添加JWT配置

application.yml

# JWT配置
jwt:
  # JWT存储的请求头
  tokenHeader: Authorization
  # JWT 加密使用的密钥
  secret: wrs-secret
  # JWT 超期限时间(60*60*24)
  expiration: 604800
  # JWT负载中拿到开头
  tokenHead: Bearer

1.3.添加JWT Token工具类

在config目录下新建component(存放Component)和security(存放配置)目录,后面有关配置就放到这两个目录中。

在这里插入图片描述

JwtTokenUtil.java

@Component
public class JwtTokenUtil {
	// 用户名的key
    private static final String CLAIM_KEY_USERNAME = "sub";
    // jwt创建时间
    private static final String CLAIM_KEY_CREATED = "created";

    /**
     * 去application.yml拿jwt密钥和jwt失效时间
     */
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据用户信息生成Token
     *
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {
        HashMap<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 从Token中获取username
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从Token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 验证Token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断Token是否失效
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        return expireDate.before(new Date());
    }

    /**
     * 从Token中获取过期时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据荷载生成JWT Token
     *
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 生成Token失效时间
     *
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }
}

1.4.添加公共返回对象

RespBean.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
    private long code;
    private String message;
    private Object object;

    /**
     * 成功返回结果
     * @param message
     * @return
     */
    public static RespBean success(String message) {
        return new RespBean(200, message, null);
    }

    /**
     * 成功返回结果
     * @param message
     * @param object
     * @return
     */
    public static RespBean success(String message, Object object) {
        return new RespBean(200, message, object);
    }

    /**
     * 失败返回结果
     * @param message
     * @return
     */
    public static RespBean error(String message) {
        return new RespBean(500, message, null);
    }

    /**
     * 失败返回结果
     * @param message
     * @param object
     * @return
     */
    public static RespBean error(String message, Object object) {
        return new RespBean(200, message, object);
    }
}

1.5.在Admin实体类中实现UserDetails

在这里插入图片描述

并实现和修改对应的方法:

Admin.java

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_admin")
@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable , UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    .
    .
    .

    @ApiModelProperty(value = "备注")
    private String remark;


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

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

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

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

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

完成之后我们新建一个Pojo类——专门传递前端传过来的用户名和密码以便登录时使用。

AdminLoginParam.java

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象", description = "")
public class AdminLoginParam {
    @ApiModelProperty(value = "用户名", required = true)
    private String username;
    @ApiModelProperty(value = "密码", required = true)
    private String password;
    @ApiModelProperty(value = "验证码", required = true)
    private String code;
}

有了这两个类之后我们就可以完成我们的登录功能

1.6.实现登录功能

先在IAdminService接口中定义登录方法,并在对应的实现类中实现。而且一般登录成功以后会获取用户信息,所以也将相关方法实现。

IAdminService.java

public interface IAdminService extends IService<Admin> {

    /**
     * 登录之后返回Token
     * @param username
     * @param password
     * @param code
     * @param request
     * @return
     */
    RespBean login(String username, String password, String code, HttpServletRequest request);

    /**
     * 根据用户名获取用户
     * @param username
     * @return
     */
    Admin getAdminByUserName(String username);
}

AdminServiceImpl.java

@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Resource
    private AdminMapper adminMapper;
    /**
     * 登录之后返回Token
     * @param username
     * @param password
     * @param code
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password, String code, HttpServletRequest request) {
        // 验证码
        String captcha = (String) request.getSession().getAttribute("captcha");
        // 判断验证码
        if ("".equals(code) || !captcha.equalsIgnoreCase(code)) {
            return RespBean.error("验证码输入有误,请重新输入!");
        }
        // 获取UserDetails
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        // 判断用户是否被禁用
        if (userDetails.isEnabled()) {
            // 前端获取的密码通过passwordEncoder与数据库中的密码对比
            if (userDetails != null && passwordEncoder.matches(password, userDetails.getPassword())) {
                // 更新security登录用户对象
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                // 将authenticationToken放入spring security全局中
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                // 生成Token
                String token = jwtTokenUtil.generateToken(userDetails);
                // 将token和头部信息存入map中,登录成功后带给前端。
                HashMap<String, String> tokenMap = new HashMap<>();
                tokenMap.put("token", token);
                tokenMap.put("tokenHead", tokenHead);
                return RespBean.success("登录成功", tokenMap);
            }
            return RespBean.error("用户名或密码错误");
        }
        return RespBean.error("该账号也被禁用,请联系管理员!");
    }

    /**
     * 根据用户名获取用户
     * @param username
     * @return
     */
    @Override
    public Admin getAdminByUserName(String username) {
        // 用户名、启用状态
        return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", username).eq("enabled", true));
    }
}

LoginController.java

@Api(tags = "LoginController")
@RestController
public class LoginController {

    @Resource
    private IAdminService adminService;

    @ApiOperation(value = "登录之后返回的Token")
    @PostMapping("/login")
    public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request) {
        return adminService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword(), adminLoginParam.getCode(), request);
    }

    @ApiOperation(value = "获取当前登录用户的信息")
    @GetMapping("/admin/info")
    public Admin getAdminInfo(Principal principal) {
        if (principal != null) {
            String username = principal.getName();
            Admin admin = adminService.getAdminByUserName(username);
            // 将用户名密码设置null,安全性。
            admin.setPassword(null);
            return admin;
        }
        return null;
    }
}

1.7.退出功能

后端只负责提供成功接口,前端实现具体功能。

LoginController.java

@ApiOperation(value = "退出登录")
@PostMapping("/logout")
public RespBean logout() {
    return RespBean.success("注销成功!");
}

1.8.配置Security

重写UserDetailsService,完善PasswordEncoder,

SecurityConfig.java

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private IAdminService adminService;
    @Resource
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
    @Resource
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }
    /**
     * 放行路径
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/captcha",
                "/login",
                "/logout",
                "/css/**",
                "/js/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
        );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 使用JWT,不需要csrf
        http.csrf().disable()
                // 使用JWT,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 所有请求都要认证
                .anyRequest().authenticated()
                .and()
                // 禁用缓存
                .headers()
                .cacheControl();
        // 添加JWT 登录授权过滤器
        http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            Admin admin = adminService.getAdminByUserName(username);
            if (admin != null) {
                return admin;
            }
            return null;
        };
    }

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

    @Bean
    public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter() {
        return new JwtAuthorizationTokenFilter();
    }

}

1.9.自定义未授权和未登录结果返回和JWT登录过滤器

JWT登录过滤器:JwtAuthorizationTokenFilter.java

public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException, NumberFormatException {
        // 获取Header
        String authHeader = request.getHeader(tokenHeader);
        // 存在token但不是tokenHead开头
        if (null != authHeader && authHeader.startsWith(tokenHead)) {
            // 字段截取authToken
            String authToken = authHeader.substring(tokenHead.length());
            // 根据authToken获取username
            String username = jwtTokenUtil.getUsernameFromToken(authToken);
            // token存在用户名但未登录
            if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
                // 登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // 验证token是否有效,如果有效,将他重新放到用户对象里。
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    // 重新设置到用户对象里
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        // 放行
        chain.doFilter(request, response);
    }
}

未授权:RestAuthorizationEntryPoint.java

@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
            throws IOException, ServletException {
       // 通过response设置编码格式
        response.setCharacterEncoding("UTF-8");
        // 设置ContentType
        response.setContentType("application/json");
        // 输出流
        RespBean bean = RespBean.error("权限不足,请联系管理员!");
        bean.setCode(403);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}

未登录:RestAuthorizationEntryPoint.java

@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException, ServletException {
        // 通过response设置编码格式
        response.setCharacterEncoding("UTF-8");
        // 设置ContentType
        response.setContentType("application/json");
        // 输出流
        PrintWriter out = response.getWriter();
        RespBean bean = RespBean.error("未登录,请登录!");
        bean.setCode(401);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}

那么到目前为止,我们的登录功能就完成了。后面可能会更改前面的配置文件,那么有遇到的时候我会一一解释。

为了与前端规范接口以及方便后端测试接口,下一篇博客我会整合Swagger2。实现根据登录用户的角色查询出对应的菜单,这涉及到RBAC权限管理,还会使用到Redis,具体请看下一篇:

👇👇👇
Spring Security结合RBAC+Redis+Swagger2实现菜单列表

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐