紧接着上一篇,那我们开始写我们的登录功能吧~
目录
- 1.登录功能
- 1.1.导入依赖
- 1.2.添加JWT配置
- 1.3.添加JWT Token工具类
- 1.4.添加公共返回对象
- 1.5.在Admin实体类中实现UserDetails
- 1.6.实现登录功能
- 1.7.退出功能
- 1.8.配置Security
- 1.9.自定义未授权和未登录结果返回和JWT登录过滤器
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实现菜单列表