1:项目背景:摸鱼之余,准备自己写一个项目,巩固一下自己。在码项目的过程中出现了一些很cd的问题,大家可能也碰到过,再次分享一下我的总结。想用springcloud+vue写一个前后端分离的项目,由于我还没有学vue,哈哈哈啊哈,所以在此用html页面做了一个简单的登录页面模仿安全登录。哈哈哈哈哈。
2:碰到的问题:自定义了登录页面,而且自定义了登录表单提交的路径(请求到controller层的登录接口),点击提交,请求就是不进入自定义的登录接口中,而是直接走入了实现了UserDetailService的方法。
(后来我重新写了一下MySercurityConfig中的configure方法中的http配置,
还有就是之前用了下面这个方法,登陆的时候去调用,然后发现总是走不到自己定义的实现了 UserDetailsService的UserDetialServiceImpl方法中,后来就把这个方法删了没用。这个其实就 是根据用户名获取用户信息的方法)
@Overrite
@Bean
public UserDeailService userDeailService() {
return username -> {
Admin admin = adminService.getAdminService(username);
return admin;
}
return null;
}
还有就是重写拦截器,生成token校验登录。
下面给大家详细的分析一下我的登录模块
3:导入依赖: 版本选择自己合适的版本 我用的是2.3.7.RELEASE
<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>
配置文件
jwt:
#JWT 存储的请求头
tokenHeader: Authorization #key
#JWT 加解密使用的签名
secret: handsome
#JWT 失效时间
expiration: 3600
#JWT 负载中拿到开头
tokenHead: Bearer #value开头
4:SpringSecurity初体验:引入依赖以后直接启动项目,访问你的项目路径就会进入到SpringSecurity自带的安全登录页面,用户名:user,密码:在项目启动后的控制台里会打印出来,可以直接登录。但是大家一般不会用SpringSecurity自带的登录页面。
5:自定义登录页面:接下来我们创建一个类继承WebSecurityConfigurerAdapter来实现登录页面自定义 代码如下。
需要先写一个登录界面:可以用html先写一个login.html,放在resourse下的static下面,再写一个controller层的登录接口,然后写一下配置类实现自定义登录页面。
package com.power.auth.securityconfig;
import com.power.auth.filter.LoginFilter;
import com.power.auth.filter.RestAuthorizationEntryPoint;
import com.power.auth.filter.RestfulAccessDenieHandler;
import com.power.auth.utils.RealPasswordEncoding;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author HandsomeZhang
* @date 2021/12/13 15:51
* Description:
*/
@Configuration//配置类注解,项目启动的时候就会加载该配置类,可以打个断点启动看看
public class MySercurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()//请求授权
.antMatchers("/loginController/getToken").permitAll()//这个是自定义表单的提交路径,也就是我们的controller层的登录接口,允许访问
.antMatchers("/login.html").permitAll()//自定义登录界面,设为允许访问
.anyRequest().authenticated()//其余请求均拦截
.and()
//自定义登录表单
.formLogin()
.loginPage("/login.html")//登录表单,但是如果不在上面授权访问不了
.loginProcessingUrl("/loginController/getToken/")//表单提交路径
.and()
.httpBasic()
.and().csrf().disable();//关闭防火墙
}
}
此时启动项目,访问项目地址,就会来到自定义的登录页面,而且是,不管你访问项目下的哪个请求路径,都会跳转到登录页面,但是加了拦截器后就不是了,需要访问登录界面,可以解决,后面说。对于非登录状态下的接口想放开的话,可以设置白名单(这个目前还没有用到)。
6:定义一个JwtToken工具类:可以生成校验token
package com.power.auth.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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";//claims会根据这个sub,解析token的时候拿到用户名,只能填写sub
private static final String CLAIM_KEY_CREATED="created";//创建时间
@Value("${jwt.secret}")
private String secret;//加解密使用的密钥
@Value("${jwt.expiration}")
private Integer 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
* @return
* 用户名不正确,或者token过期都视为无效
* 返回true为有效
*/
public boolean tokenExpired(String token, UserDetails userDetails) {
String userName = getUserNameFromToken(token);
return userName.equals(userDetails.getUsername()) && isTokenExpired(token);
}
/**
* 判断token 能否被刷新
* 如果token过期了则可以被刷新
* @param token
* @return
* false为过期
*/
public boolean isRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token过期时间
* @param token
* @return
*/
public String refreshTokenExpiredTime(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) {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();//获取的是生成token时放进去的失效日期
return new Date().before(expiration);
}
/**
* 根据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) {
byte[] secretKey = secret.getBytes();
//生成token
return Jwts.builder()
.setClaims(claims)//荷载
.setExpiration(generateExpirationDate())//失效时间
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
/**
* 生成token失效时间
* @return
*/
private Date generateExpirationDate() {
return new Date(120000);//过期时间两个小时
}
}
7:我这里重写了sercurity的密码加密规则,我这里使用的是md5工具类,网上可以找到
package com.power.auth.utils;
import com.power.common.utils.MD5Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author HandsomeZhang
* @date 2021/12/12 13:08
* Description:
*/
public class RealPasswordEncoding implements PasswordEncoder {
public static Logger logger = LoggerFactory.getLogger(RealPasswordEncoding.class);
/**
* 密码加密
* @param charSequence
* @return
*/
@Override
public String encode(CharSequence charSequence) {
logger.info("自定义密码加密>>>>>>>>");
String passwordSafe = null;
try {
String password = (String) charSequence;
passwordSafe = MD5Utils.md5Encode(password);
} catch (Exception e) {
logger.info("加密失败");
e.printStackTrace();
}
return passwordSafe;
}
/**
* 密码校验
* @param charSequence 未加密密码
* @param s 已加密密码
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
logger.info("自定义密码校验>>>>>>>>");
String passwordSafe = null;
try {
String password = (String) charSequence;
passwordSafe = MD5Utils.md5Encode(password);
String s1 = MD5Utils.md5Encode("Zjg123...");
logger.info(s1);
} catch (Exception e) {
logger.info("加密失败");
e.printStackTrace();
}
logger.info(passwordSafe);
String password = (String) charSequence;
boolean b = MD5Utils.matches(password, s);
if (b == true) {
logger.info("密码比对成功");
} else {
logger.info("密码比对失败");
}
return b;
}
}
8:定义一个UserDetails的实现类代替springsecurity自带的User,方便接收自己想要的参数
package com.power.auth.entity;
import com.power.basic.entity.User;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
/**
* 自定义LoginUserDetials取代User可以放更多的信息
*/
@Data
@NoArgsConstructor
public class LoginUserDetials implements UserDetails, Serializable {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private boolean isEnabled;
public LoginUserDetials(User user, Collection<? extends GrantedAuthority> authorities) {
this.setUsername(user.getUserName());
this.setId(user.getUserId());
this.setPassword(user.getUserPassword());
this.setAuthorities(authorities);
this.setAccountNonExpired(true);
this.setAccountNonLocked(true);
this.setCredentialsNonExpired(true);
this.setEnabled(true);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.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;
}
}
9:校验登录用户信息,实现security自带的UserDetailsService接口
package com.power.auth.serviceimpl;
import com.alibaba.fastjson.JSONObject;
import com.power.auth.entity.LoginUserDetials;
import com.power.auth.utils.JwtTokenUtil;
import com.power.basic.api.UserServiceFeign;
import com.power.basic.entity.User;
import com.power.common.pojo.ResultPublic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author HandsomeZhang
* @date 2021/12/13 16:57
* Description:
*/
@Service
public class UserDetialServiceImpl implements UserDetailsService {
//从数据库
@Autowired(required = false)
private UserServiceFeign userServiceFeign;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead;
//校验登录用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("userName", username);
//查询数据判断用户名是否存在
ResultPublic<User> getUser = userServiceFeign.getByUserName(jsonObject);
User user = getUser.getData();
if (user == null ) {
throw new UsernameNotFoundException("用户名不存在");
}
//把查询出来的密码进行解析,注册时已经进行过加密
LoginUserDetials loginUserDetials = new LoginUserDetials(user,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
//判断用户状态是否正常
if (!loginUserDetials.isEnabled()) {
throw new DisabledException("该账户已被禁用!");
} else if (!loginUserDetials.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定!");
}
return loginUserDetials;
}
//登录时创建token
public ResultPublic createToken (UserDetails userDetails, String password) {
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return ResultPublic.fail("用户名或者密码不正确");
}
//登陆对象放进spring security全文中,不放可能会出现一些问题
//更新security登录用户对象
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//参数:用户信息,密码,权限列表,
SecurityContextHolder.getContext().setAuthentication(authenticationToken);//将token放入Security全局中
//生成token
Map<String, Object> tokenMap = new HashMap<>();
String token = jwtTokenUtil.generateToken(userDetails);
tokenMap.put("token", token);//放入token
tokenMap.put("tokenHead", tokenHead);//放入Bearer
//登录成功后,前端会将map中的两个值拼接后,以后请求都会带在请求头中
return ResultPublic.success("登录成功", tokenMap);
}
}
10:定义一个登录接口:每次登录,都会生成一个token
package com.power.auth.controller;
import com.power.auth.serviceimpl.UserDetialServiceImpl;
import com.power.common.pojo.ResultPublic;
import io.swagger.annotations.Api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Api(tags = "LoginController")
@RestController
@RequestMapping(path = "/loginController")
public class LoginController {
@Resource
private UserDetailsService userDetailsService;
@Autowired
private UserDetialServiceImpl userDetialServiceImpl;
public static Logger logger = LoggerFactory.getLogger(LoginController.class);
@PostMapping(path = "/getToken")
public ResultPublic getToken(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request
) {
//数据库查询比对登录用户状态信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//生成token
ResultPublic token = userDetialServiceImpl.createToken(userDetails, password);
return token;
}
@GetMapping(path = "/loginOut")
public ResultPublic loginOut() {
return ResultPublic.success("成功退出");
}
}
11:定义一个拦截器,每次访问接口的时候,都会经过该拦截器,验证token是否失效等
package com.power.auth.filter;
import com.power.auth.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author HandsomeZhang
* @date 2021/12/13 20:39
* Description:
*/
public class LoginFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader;//前端带来的请求头
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
public static Logger logger = LoggerFactory.getLogger(LoginFilter.class);
//每次访问都来到这里拦截并校验token
//如果存在token并且用户状态正常的话,但是token过期,则重新设置用户对象
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("进入拦截器JwtAuthencationTokenFilter!!!");
//前端传来的token 例:Authorization = Bearer+jwt工具类生成的令牌
String fakeToken = request.getHeader(tokenHeader);
//如果token不为空,并且token以Bearer开头
if (null != fakeToken && fakeToken.startsWith(tokenHead)) {
String authToken = fakeToken.substring(tokenHeader.length());//截取掉Bearer拿到后面的token
String userName = jwtTokenUtil.getUserNameFromToken(authToken);//从token中获取用户名
//token存在用户名但未登录
if (null != userName && null == SecurityContextHolder.getContext().getAuthentication()) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);//根据用户名查的用户信息
//验证token是否过期,重新设置用户对象
if (jwtTokenUtil.tokenExpired(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
logger.info("JwtAuthencationTokenFilter拦截器放行!!!");
filterChain.doFilter(request, response);
}
}
12:还可以自己定义两个异常处理方案:我这里定义了RestAuthorizationEntryPoint和RestfulAccessDenieHandler在MySercurityConfig里面使用