提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
登录功能主要用到以下两种依赖:
spring-boot-starter-security
jjwt
1、spring-boot-starter-security是Spring Boot框架中的一个Starter依赖项,用于集成和提供安全功能。它基于Spring Security项目,可以帮助您轻松地实现身份验证(Authentication)和授权(Authorization)功能。
以下是依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、jjwt(Java JWT)是一个用于创建和验证JSON Web Tokens(JWT)的Java库。JWT是一种开放标准(RFC 7519),用于在不同的实体之间安全传输信息,通常用于身份验证和授权场景。
以下是依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
配置:
jwt :
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间《60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
创建好的代码如下(示例):
package com.xiaocheng.server.config.security;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* jwt工具类
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME="sub";
private static final String CLAIM_KEY_CREATED="created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据用户信息生成Token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails){
Map<String,Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
/**
* 从token中获取登录用户名
* @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
* @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 canRefresh(String token){
return isTokenExpired(token);
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token){
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
/**
* 判断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();
}
/**
* 从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;
}
/**
* 根据荷载生成Jwt Token
* @param claims
* @return
*/
private String generateToken(Map<String,Object> claims){
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.ES512,secret)
.compact();
}
/**
* 生成token失效时间
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis()+expiration*1000);
}
}
整体思路:
前端传用户名和密码给后端——>
后端先去校验用户名和密码——>1、有错误则返回
——>2、正确:生成jwt令牌并返回给前端——>前端拿到jwt令牌就会放在请求头里(后面的任何请求都会携带jwt令牌)——>后端会有拦截器对jwt令牌进行验证——>1、验证通过后才能返回结果 ——>2、如果验证未通过说明是token过期或者非合法登录用户
使用公共返回对象的好处:
使用公共返回对象可以提供一致的接口响应结果,简化接口设计和维护工作,统一错误处理,提高代码的可读性、可维护性和复用性,同时也方便前后端之间的协作
package com.xiaocheng.server.pojo;
import com.baomidou.mybatisplus.extension.api.R;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @param message
* @return
*/
public static RespBean success(String message){
return new RespBean(200,message,null);
}
/**
* 成功返回结果
* @param message
* @return
*/
public static RespBean success(String message,Object obj){
return new RespBean(200,message,obj);
}
/**
* 失败返回
* @param message
* @return
*/
public static RespBean error(String message){
return new RespBean(500,message,null);
}
/**
* 失败返回
* @param message
* @return
*/
public static RespBean error(String message,Object obj){
return new RespBean(500,message,obj);
}
}
package com.xiaocheng.server.service.impl;
import com.xiaocheng.server.config.security.JwtTokenUtil;
import com.xiaocheng.server.pojo.Admin;
import com.xiaocheng.server.mapper.AdminMapper;
import com.xiaocheng.server.pojo.RespBean;
import com.xiaocheng.server.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
AdminServiceImpl实现类的代码如下:
/**
*
* 服务实现类
*
*
* @author xiaocheng
* @since 2023-06-05
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 登陆之后返回token
* @param username
* @param password
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(null==userDetails||!passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请联系管理员");
}
//更新security登录用户对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead);
return RespBean.success("登陆成功",tokenMap);
}
}
需要在Admin内添加UserDetail的实现类,
需要添加的UserDetail的原因如下:
Admin类实现了Serializable和UserDetails接口。让我们逐个了解这两个接口的含义和作用:
Serializable接口:Serializable接口是Java中的一个标记接口,用于标识一个类的对象可以被序列化(即可以在网络中传输或持久化到磁盘)。通过实现Serializable接口,Admin类的对象可以被转换为字节流并在网络中传输或保存到文件系统中。
UserDetails接口:UserDetails接口是Spring Security框架中定义的接口,用于表示用户的详细信息。通过实现UserDetails接口,Admin类可以作为用户对象在Spring Security中进行认证和授权。UserDetails接口定义了一组方法,用于获取用户的用户名、密码、权限等信息。
通过实现Serializable接口,Admin类的对象可以在不同的环境中进行序列化和反序列化操作。而通过实现UserDetails接口,Admin类可以在Spring Security中使用,以提供用户认证和授权的功能。
请注意,这两个接口只是在Admin类中的实现,具体的方法和功能需要根据实际的代码来确定。Serializable接口和UserDetails接口在不同的上下文中有不同的用途和功能。
添加后的代码如下
package com.xiaocheng.server.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Collection;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
*
*
*
*
* @author xiaocheng
* @since 2023-06-05
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@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 name;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "住宅电话")
private String telephone;
@ApiModelProperty(value = "联系地址")
private String address;
@ApiModelProperty(value = "是否启用")
private Boolean enabled;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "用户头像")
@TableField("userFace")
private String userFace;
@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类
代码如下:
package com.xiaocheng.server.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户登录实体类
*/
@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;
}
再可以从controller层写起
先写LoginController,完成登录后返回token,获取当前用户信息,退出登录三个功能
代码如下:
package com.xiaocheng.server.controller;
import com.xiaocheng.server.pojo.Admin;
import com.xiaocheng.server.pojo.AdminLoginParam;
import com.xiaocheng.server.pojo.RespBean;
import com.xiaocheng.server.service.IAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
/**
* 登录
*/
@Api(tags = "LoginController")
@RestController
public class LoginController {
private IAdminService adminService;
@ApiOperation(value = "登录后返回token")
@PostMapping("/login")
public RespBean login(AdminLoginParam adminLoginParam, HttpServletRequest request){
return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
}
@ApiOperation(value = "获取当前用户信息")
@GetMapping("/admin/info")
public Admin getAdminInfo(Principal principal){
if (null==principal){
return null;
}
String username = principal.getName();
Admin admin = adminService.getAdminByUserName(username);
admin.setPassword(null);
return admin;
}
@ApiOperation(value = "退出登录")
@PostMapping("/logout")
public RespBean logout(){
return RespBean.success("注销成功!");
}
}
再到service层补充IAdminService类的方法
代码如下:
package com.xiaocheng.server.service;
import com.xiaocheng.server.pojo.Admin;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xiaocheng.server.pojo.RespBean;
import javax.servlet.http.HttpServletRequest;
/**
*
* 服务类
*
*
* @author xiaocheng
* @since 2023-06-05
*/
public interface IAdminService extends IService<Admin> {
/**
* 登陆之后返回token
* @param username
* @param password
* @param request
* @return
*/
public RespBean login(String username, String password, HttpServletRequest request);
/**
* 根据用户名获取用户
* @param username
* @return
*/
Admin getAdminByUserName(String username);
}
根据service补充实现类
AdminServiceImpl的代码如下
package com.xiaocheng.server.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaocheng.server.config.security.JwtTokenUtil;
import com.xiaocheng.server.pojo.Admin;
import com.xiaocheng.server.mapper.AdminMapper;
import com.xiaocheng.server.pojo.RespBean;
import com.xiaocheng.server.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
*
* 服务实现类
*
*
* @author xiaocheng
* @since 2023-06-05
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 登陆之后返回token
* @param username
* @param password
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, HttpServletRequest request) {
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(null==userDetails||!passwordEncoder.matches(password,userDetails.getPassword())){
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请联系管理员");
}
//更新security登录用户对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//生成token
String token = jwtTokenUtil.generateToken(userDetails);
Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("tokenHead",tokenHead);
return RespBean.success("登陆成功",tokenMap);
}
/**
* 根据用户名获取用户
* @param username
* @return
*/
@Override
public Admin getAdminByUserName(String username) {
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username).eq("enabled",true));
}
}
现在先进行security的配置
package com.xiaocheng.server.config.security;
import com.xiaocheng.server.pojo.Admin;
import com.xiaocheng.server.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Security配置类
*
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IAdminService adminService;
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用JWT,不需要csrf
http.csrf()
.disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//允许登录访问
.antMatchers("/login","/logout")
.permitAll()
//除了上面,所有请求都要求认证
.anyRequest()
.authenticated()
.and()
//禁用缓存
.headers()
.cacheControl();
//添加jwt,登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
@Override
@Bean
public UserDetailsService userDetailsService(){
return username ->{
Admin admin = adminService.getAdminByUserName(username);
if(null!=admin){
return admin;
}
return null;
};
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public JWTAuthencationTokenFilter jwtAuthencationTokenFilter(){
return new JWTAuthencationTokenFilter();
}
}
因为要添加jwt,登录授权过滤器
//添加jwt,登录授权过滤器
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
所以创建以下文件
以下是jwtAuthencationTokenFilter的代码:
package com.xiaocheng.server.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.WebApplicationInitializer;
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;
/**
* Jwt登录授权过滤器
*/
public class JWTAuthencationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String autheader = httpServletRequest.getHeader(tokenHeader);
//存在token
if(null!=autheader&&autheader.startsWith(tokenHead)){
String authToken = autheader.substring(tokenHead.length());
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(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
又因为要添加自定义未授权和未登录结果返回
所以创建一下两个类
RestAuthorizationEntryPoint:当未登录或token失效时访问接口时,自定义的返回结果
代码如下:
package com.xiaocheng.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xiaocheng.server.pojo.RespBean;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当未登录或token失效时访问接口时,自定义的返回结果
*/
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
PrintWriter out = httpServletResponse.getWriter();
RespBean bean = RespBean.error("尚未登录,请登录!");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
RestfulAccessDeniedHandler:当访问接口没有权限时,自定义返回结果
代码如下:
package com.xiaocheng.server.config.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xiaocheng.server.pojo.RespBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 当访问接口没有权限时,自定义返回结果
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
PrintWriter out = httpServletResponse.getWriter();
RespBean bean = RespBean.error("权限不足,请联系管理员!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
至此登录的逻辑完成