SpringBoot+Vue.js项目中使用Spring Security+JWT实现登录权限控制
目标和需求我们的目的是在 SpringBoot+Vue.js 这样的前后端分离的项目中增加一个登录权限控制模块。这个模块的需求如下:包含多个角色,每个角色所具有的权限不同(可以看到不同的菜单和页面)对整个系统做登录控制,即未登录强制跳转到登录页面登出功能实现方案:登录控制使用 Spring Security 框架,登录控制使用 JWT 实现,登录成功后使用 JWT 生成 token...
目标和需求
我们的目的是在 SpringBoot+Vue.js 这样的前后端分离的项目中增加一个登录权限控制模块。这个模块的需求如下:
- 包含多个角色,每个角色所具有的权限不同(可以看到不同的菜单和页面)
- 对整个系统做登录控制,即未登录强制跳转到登录页面
- 登出功能
实现方案:
登录控制使用 Spring Security 框架,登录控制使用 JWT 实现,登录成功后使用 JWT 生成 token 返回前端,前端所有请求都在 cookie 中带上 token 到后端校验。
Spring Security原理
概述
Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
框架原理
Spring Security 中主要通过 Filter 拦截 http 请求实现权限控制。
以下为框架自带主要过滤器:
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- LogoutFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- UsernamePasswordAuthenticationFilter
- BasicAuthenticationFilter
框架核心组件如下:
- SecurityContextHolder:提供对SecurityContext的访问
- SecurityContext:持有Authentication对象和其他可能需要的信息
- AuthenticationManager 其中可以包含多个AuthenticationProvider
- ProviderManager对象为AuthenticationManager接口的实现类
- AuthenticationProvider:主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
- Authentication:Spring Security方式的认证主体
- GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
- UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
- UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象
根据业务需求,这里我们继承UsernamePasswordAuthenticationFilter
自定义过滤器作为后端拦截登录请求的过滤器。
由于Spring Security没有自带解析 token 的过滤器,因此我们需要自己实现对 JWT 适配的鉴权过滤器,通过继承 BasicAuthenticationFilter
实现自定义鉴权过滤器。
自定义安全配置
通过实现WebSecurityConfigurerAdapter
中的configure(HttpSecurity http)
,可以自定义安全配置,比如装配自定义过滤器,设置除部分请求外均需要鉴权,设置一些回调Handler
等。
JWT原理
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。
其原理是将用户信息的JSON字符串加密生成唯一的token返回给前端,后端通过解析token来验证是否已登录。
模块实现
数据库表设计
一共涉及四个表,分别为用户表,角色表,菜单表以及用户角色关联表。
其中角色与菜单一一对应。
前端实现
前端使用Vue.js+Element-UI实现,对于菜单的存储,使用了Element-UI中的树形组件产生的value值,实际为一个JSON字符串,前端通过获取该字符串,解析得到菜单信息,显示具体的菜单按钮。
token使用cookie保存,浏览器会自动将cookie携带在每一个请求中。
toFirstPage(level) {//根据角色level选择进入默认页面
if(level === '999'){
this.$router.push({ path: "/enterpriseManage" });
}else if(level === '800'){
this.$router.push({ path: "/enterpriseAccount" });
}else{
this.$router.push({ path: "/extAccount" });
}
},
async checkLogin(loginUser){
try {
let username = this.loginUser.username;
var res = await checkUsernameAndPassword({
username: username,
password: this.loginUser.password,
});
if(res.resultCode == '0'){
window.name = this.$moment(new Date()).format("YYYY-MM-DD HH:mm:ss");
//全局缓存登录的用户信息
this.$store.baseStore.commit('setUserInfo', res.result);
this.$store.baseStore.commit('setWindowname', window.name);
if(res.result.managerLevel ==='999'){
this.$store.baseStore.dispatch('getAllDeptId');
}
this.toFirstPage(res.result.managerLevel);
} else {
this.$message({
type: 'error',
message: res.result
});
}
} catch (error) {
this.$message({
type: 'error',
message: error
});
}
},
上面的代码是前端发送登录请求的方法,具体请求路径保存在checkUsernameAndPassword
变量中,登录通过后,通过调用baseStore
中的方法存储登录信息,并且根据角色跳转到对应的路由中。
baseStore中的部分方法如下,
Vue.use(Vuex);
export const baseStore = new Vuex.Store({
//全局缓存
state: {
userInfo:{},
},
mutations: {
setUserInfo(state, payload) {
//保存到浏览器缓存
window.localStorage.setItem("userInfo", JSON.stringify(payload));
state.userInfo = payload;
},
},
getters: {
getUserInfo: state => {
if(state.userInfo == undefined || state.userInfo.username == undefined){
var userInfo = window.localStorage.getItem("userInfo");
if(userInfo == undefined){
return new Object();
}else{
state.userInfo=JSON.parse(userInfo);
return state.userInfo;
}
}else{
return state.userInfo
}
},
},
});
在登录成功进入到默认页面之前,我们需要解析登录信息,得到菜单信息以显示特定的菜单。
menu.vue
created() {
// 得到登录信息
let userInfo = this.$store.baseStore.getters.getUserInfo;
//从菜单信息中得到一级菜单信息
let menu = userInfo.menu.firstmenuset;
//逐个查找菜单项是否在firstmenuset中
if(menu.indexOf("账号详情") != -1){
this.$data.items.push({ topage: '/extAccount', userName: 'iconfont ucc-shouye2 flew-left-menuicon', text: '账号详情', modelName: 'extAccount', });
}
},
除了登录成功之后的操作外,登录失败则跳转到登录页面,前面代码中我们的登录请求是这样发送的:
login.vue
import {
checkUsernameAndPassword,
}from '@/api/getData';
const res = await checkUsernameAndPassword({
username: username,
password: this.loginUser.password,
});
checkUsernameAndPassword 是从getData.js中导入的
getData.js
import fetch from '@/config/fetch'
export const checkUsernameAndPassword = (args) => fetch('/login', args, 'POST');
这里的fetch方法实际上是所有请求发送的方法,我们可以在这里首先解析后端返回码,若为未登录,则跳转到登录页面:
import { baseUrl } from './env'
import router from '../router'
export default async(url = '', data = {}, type = 'GET', method = 'fetch') => {
//省略其他代码
......
let requestConfig = {
credentials: 'include',
method: type,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
},
mode: "cors",
cache: "no-cache"
}
if (type == 'POST') {
requestConfig.body=dataStr;
}
try {
const response = await fetch(url, requestConfig);
const responseJson = await response.json();
if(responseJson.resultCode == "00015"){
router.push("login");
}else{
return responseJson;
}
} catch (error) {
throw new Error(error);
}
......
}
后端实现
后端我们需要实现:
- 登录过滤器JWTLoginFilter
- 请求鉴权器JWTAuthenticationFilter
- 登录认证器CustomAuthenticationProvider
- 自定义配置SecurityConfig
- 鉴权失败Handler:GoAuthenticationEntryPoint
- 登录成功Handler:GoAuthenticationSuccessHandler
首先要准备基础设施,相关的Bean,验证账号密码的Service以及token的生成等。需要注意的是,用户信息Bean必须继承Spring Security中的UserDetails
。Service需要实现UserDetailsService
中的loadUserByUsername(String userName)
方法。
这里只贴token的生成:
public class JWTTokenUtil {
public static final String SECRET = "spring security Jwt Secret";
public static final String BEARER = "Bearer:";
public static final String AUTHORIZATION = "Authorization";
public static String getToken(JSONObject user) {
return Jwts.builder()
.setSubject(JSONObject.toJSONString(user))
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
}
登录过滤器,继承Spring Security自带的用户名密码过滤器,实现对登录请求的拦截和转发。
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
// 接收并解析用户凭证
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
User user = new User();
user.setUsername(req.getParameter("username").trim());
user.setPassword(req.getParameter("password"));
return authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
}
}
请求鉴权器,拦截所有请求,从cookie中查询token信息,并进行解析鉴定。
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
Cookie [] cookies = request.getCookies();
String authorization = "";
if(cookies != null){
for(Cookie cookie : cookies){
if(JWTTokenUtil.AUTHORIZATION.equals(cookie.getName())){
authorization = cookie.getValue();
}
}
}
if ("".equals(authorization)) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(authorization);
if(authentication == null){
chain.doFilter(request, response);
return;
}
//保存到spring security上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(String authorization) {
// parse the token.
try {
String userJson = Jwts.parser().setSigningKey(JWTTokenUtil.SECRET)
.parseClaimsJws(authorization.replace(JWTTokenUtil.BEARER, "")).getBody().getSubject();
User user = JSONObject.parseObject(userJson, User.class);
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
} catch (MalformedJwtException | ExpiredJwtException e) {
//token解析失败,token过期
return null;
}
return null;
}
}
登录认证器,负责将登录过滤器拦截得到的登录账号密码进行验证。
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService service;
public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
this.service = userDetailsService;
}
/**
* 验证类
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
User userDetails = null;
if(username != null) {
//调用相应service从数据库获取对应用户信息
userDetails = (User) service.loadUserByUsername(username);
}
if(userDetails == null) {
throw new UsernameNotFoundException("用户名或密码无效");
}else if (!userDetails.isEnabled()){
throw new DisabledException("用户已被禁用");
}else if (!userDetails.isAccountNonExpired()) {
throw new AccountExpiredException("账号已过期");
}else if (!userDetails.isAccountNonLocked()) {
throw new LockedException("账号已被锁定");
}else if (!userDetails.isCredentialsNonExpired()) {
throw new LockedException("凭证已过期");
}
String password = userDetails.getPassword();
if(!password.equals(Md5.encodeByMD5((String)token.getCredentials()))) {
throw new BadCredentialsException("用户名/密码无效");
}
return new UsernamePasswordAuthenticationToken(userDetails, password,userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
登录成功和失败的回调处理器,可以在这两个处理器中实现cookie的存储,失败状态码的返回等
public class GoAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
Result res = null;
response.setCharacterEncoding("UTF-8");
if(exception instanceof InsufficientAuthenticationException){
res = new Result("00015",Result.FAILURE,exception.getMessage());
}else{
res = new Result("00010",Result.FAILURE,"未知权限错误");
}
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = response.getWriter();
out.write(JSONObject.toJSONString(res));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
public class GoAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
User user = (User)authentication.getPrincipal();
//返回前端的token
JSONObject userToken = new JSONObject();
userToken.put("username", user.getUsername());
cookie.setPath("/ucc");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.addCookie(cookie);
response.addCookie(new Cookie(JWTTokenUtil.AUTHORIZATION, JWTTokenUtil.BEARER + JWTTokenUtil.getToken(userToken)));
Result result = new Result("0",Result.SUCCESS,user);
PrintWriter out = null;
try {
out = response.getWriter();
out.write(JSONObject.toJSONString(result));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
最后是Spring Security的自定义配置类,将配置一些权限管理的规则以及整合上面各项功能类。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class securityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService service;
@Bean
public AuthenticationProvider authenticationProvider(){
AuthenticationProvider authenticationProvider=new CustomAuthenticationProvider(service);
return authenticationProvider;
}
/**
* 验证用户权限的方法
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTLoginFilter jwtLoginFilter= new JWTLoginFilter(authenticationManager());
JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter(authenticationManager());
//设置回调Handler
jwtLoginFilter.setAuthenticationSuccessHandler(new GoAuthenticationSuccessHandler());
http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling() //异常处理
.authenticationEntryPoint(new GoAuthenticationEntryPoint())
.and().authorizeRequests()
.antMatchers().permitAll()//可以设置不需要认证的请求
.anyRequest().authenticated()
.and()
.addFilter(jwtLoginFilter)
.addFilter(authenticationFilter)
.logout() //拦截登出
.logoutUrl("/logout")//登出URL
.logoutSuccessHandler(new GoLogoutSuccessHandler()) //登出成功回调函数
.invalidateHttpSession(true)
.deleteCookies(JWTTokenUtil.AUTHORIZATION);
}
}
更多推荐
所有评论(0)