目录
登录总体流程
基于JWT和RSA的Token机制
JWT简介
RSA简介
SpringSecurity登录处理的配置
后端过滤器的Token解析
前端拦截器处理
前后端分离登录和常规后台登录的区别
1.前后端交互方式不同
常规登录: 通过页面的跳转和模型数据绑定
前后端分离: 通过Ajax和JSON进行通信
2.登录状态跟踪方式不同
常规登录: 通过Session和Cookie保存用户状态
前后端分离: 通过Token保持用户状态
前后端分离的优点
1.彻底解放前端
前端不再需要向后台提供模板或是后台在前端html中嵌入后台代码
2.提高工作效率,分工更加明确
前后端分离的工作流程可以让前端管签单,后端管后端,前后端开发同时进行,增加了开发的灵活性。
3.局部性能提升
通过前端路由的配置,可以实现页面的按需加载,不用一开始就加载所有的资源,提升了用户体验。
4.降低维护成本
通过MVC框架,可以快速的定位问题所在,客户端的问题不再需要后台人员参与及调试,增强了代码的重构及可维护性。
总体流程
JWT(JSON Web Tokens )是JSON格式的加密字符串,用于加密验证信息,在前后端进行通信
分为三个部分
1) 头部
2) 负载
3) 指纹
需要的依赖
io.jsonwebtoken
jjwt
0.9.0
joda-time
joda-time
2.9.9
JWT工具类
/**
* JWT工具类
*/
public class JwtUtil {
public static final String JWT_KEY_USERNAME = "username";
public static final int EXPIRE_MINUTES = 120;
/**
* 私钥加密token
*/
public static String generateToken(String username, PrivateKey privateKey, int expireMinutes) {
return Jwts.builder()
.claim(JWT_KEY_USERNAME, username)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 从token解析用户
*
* @param token
* @param publicKey
* @return
* @throws Exception
*/
public static String getUsernameFromToken(String token, PublicKey publicKey){
Jws claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = (String) body.get(JWT_KEY_USERNAME);
return username;
}
}
RSA是一种非对称式的加密算法
对称式加密只有一个秘钥,加密和解密都通过该秘钥完成
非对称式加密有两个秘钥,公钥和私钥,加密和解密由公钥和私钥分开完成
/**
* RSA工具类
*/
public class RsaUtil {
public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/";//秘钥保存位置
public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa";//公钥路径
public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa";//私钥路径
public static PublicKey publicKey; //公钥
public static PrivateKey privateKey; //私钥
/**
* 类加载后,生成公钥和私钥文件
*/
static {
try {
File rsa = new File(RSA_PATH);
if (!rsa.exists()) {
rsa.mkdirs();
}
File pubKey = new File(RSA_PUB_KEY_PATH);
File priKey = new File(RSA_PRI_KEY_PATH);
//判断公钥和私钥如果不存在就创建
if (!priKey.exists() || !pubKey.exists()) {
//创建公钥和私钥文件
RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
}
//读取公钥和私钥内容
publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(1024, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
响应状态枚举
/**
* 响应状态枚举
*/
public enum ResponseStatus {
/**
* 内置状态
*/
OK(200,"操作成功"),
INTERNAL_ERROR(500000,"系统错误"),
BUSINESS_ERROR(500001,"业务错误"),
LOGIN_ERROR(500002,"账号或密码错误"),
NO_DATA_ERROR(500003,"没有找到数据"),
PARAM_ERROR(500004,"参数格式错误"),
AUTH_ERROR(401,"没有权限,需要登录");
private Integer code;
private String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
ResponseStatus(Integer status, String message) {
this.code = status;
this.message = message;
}
响应数据封装对象
/**
* 响应数据封装对象
*/
@Data
@ApiModel(value = "ResponseResult对象", description = "前端数据封装对象")
public class ResponseResult {
/**
* 状态信息
*/
@ApiModelProperty("响应状态")
private ResponseStatus status;
/**
* 数据
*/
@ApiModelProperty("数据")
private T data;
public ResponseResult(ResponseStatus status, T data) {
this.status = status;
this.data = data;
}
/**
* 返回成功对象
* @param data
* @return
*/
public static ResponseResult ok(T data){
return new ResponseResult<>(ResponseStatus.OK, data);
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult error(ResponseStatus status){
return new ResponseResult<>(status,status.getMessage());
}
/**
* 返回错误对象
* @param status
* @return
*/
public static ResponseResult error(ResponseStatus status,String msg){
return new ResponseResult<>(status,msg);
}
/**
* 向流中输出结果
* @param resp
* @param result
* @throws IOException
*/
public static void write(HttpServletResponse resp, ResponseResult result) throws IOException {
resp.setContentType("application/json;charset=UTF-8");
String msg = new ObjectMapper().writeValueAsString(result);
resp.getWriter().print(msg);
resp.getWriter().close();
}
}
用户信息
@Data
public class UserVO {
private String username;
private String token;
}
验证成功处理器
/**
* 登录成功处理器
*/
@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//获得用户名
User user = (User) authentication.getPrincipal();
//将用户名生成jwt token
String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
//将token 发送给前端
UserVO userVo = new UserVO(user.getUsername(),token);
ResponseResult.write(response,ResponseResult.ok(userVo));
log.info("user:{} token:{}",user.getUsername() , token);
}
}
SpringSecucrity相关配置
/**
* SpringSecucrity相关配置
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
/**
* 密码加密器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 设置自定义登录逻辑
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 页面资源的授权
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//对请求进行授权
http
.authorizeRequests()
.antMatchers(
"/swagger-ui.html","/v2/**","/swagger-resources/**","/webjars/springfox-swagger-ui/**", //放行swagger相关
"/js/**","/css/**", //放行静态资源
"/login","logout" //放行登录和登出
)
.permitAll()
.anyRequest()
.authenticated() //其它的请求需要登录
.and()
.formLogin()//登录配置
.successHandler(new LoginSuccessHandler()) //登录成功处理
.failureHandler((req,resp,auth) -> {//进行登录失败的处理
ResponseResult.write(resp,ResponseResult.error(ResponseStatus.LOGIN_ERROR));
})
.and()
.exceptionHandling() //未进行登录请求的处理
.authenticationEntryPoint((req,resp,auth)->{
ResponseResult.write(resp,ResponseResult.error(ResponseStatus.AUTH_ERROR));
})
.and()
.logout()//登出配置
.logoutSuccessHandler((req,resp,auth) -> {
ResponseResult.write(resp,ResponseResult.ok("注销成功"));
})
.clearAuthentication(true)
.and()
.cors() //跨域配置
.configurationSource(corsConfigurationSource())
.and()
.csrf()
.disable() //关闭CSRF防御
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不使用session
.and()
.addFilter(new TokenAuthenticationFilter(authenticationManager())); //添加token验证过滤器
}
/**
* 跨域配置对象
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//配置允许访问的服务器域名
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
public static final String HEADER = "Authorization";
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得前端请求中的token
String token = request.getHeader(HEADER);
if(StringUtils.isBlank(token)){
token = request.getParameter(HEADER);
}
//如果token为空,放行,验证失败
if(StringUtils.isBlank(token)){
chain.doFilter(request,response);
return;
}
try {
//解析token
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
if (StringUtils.isNotBlank(username)) {
//把用户token放入SecurityContext,通过验证
List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("");
User user = new User(username, "", authorities);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}catch (ExpiredJwtException e){
log.error("token过期",e);
}catch (Exception ex){
log.error("token解析错误",ex);
}
chain.doFilter(request,response);
}
}
handleLogin() {
if (!this.username || !this.password) {
return this.$msg.warning('用户名和密码不能为空')
}
let user = this.qs.stringify({username: this.username, password: this.password})
console.log(user)
this.axios.post('/login', user).then(result => {
console.log(result)
if (result.data.status === 'OK') {
this.$msg.success('登录成功')
localStorage.setItem('username',result.data.data.username)
localStorage.setItem('token',result.data.data.token)
this.$router.push({path: '/admin'})
} else {
this.$msg.error(result.data.data)
}
})
}
main.js
//错误响应拦截
axios.interceptors.response.use(res => {
console.log('拦截响应');
console.log(res);
if( res.data.status === 'OK'){
return res;
}
if( res.data.data === '没有权限,需要登录' ){
MessageBox.alert('没有权限,需要登录','权限错误',{
confirmButtonText:'跳转登录页面',
callback: action => {
window.location.href = '/'
}
})
}else{
Message.error(res.data.data)
}
})
//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
config => {
let token = localStorage.getItem("token");
console.log("token:" + token);
if (token) {
//把localStorage的token放在Authorization里
config.headers.Authorization = token;
}
return config;
},
function(err) {
console.log("失败信息" + err);
}
);