整体思路
- 前端页面登录,绑定对象,发送axious到后端,进行验证
- 后端接收到前端的信息,此时密码为明文,而数据库中密码为密文,只能通过用户名查询数据库,判断用户是否存在,如果存在,通过BCryptPasswordEncoder对密码进行验证
- 验证密码通过后,查询用户的角色和权限信息,放到用户对象中,此时只需要字符串即可
- 通过私钥进行加密,颁发令牌到前端
- 前端接收到登录后的响应,判断是否成功登录,如果成功将token存在localstorage中,然后通过路由跳转页面到首页,登录操作完毕
- 此时访问其他资源会报403,因为springsecurity的上下文中并没有用户信息,用户角色和用户的权限,这个工作由springsecurity的FilterSecurityInterceptor这个过滤器来执行,我们需要在basicAuthenticationFilter这个过滤器生效时重写,验证token并将token中的用户信息放到springsecurity的上下文中
- 重写basicAuthenticationFilter时注意,此过滤器在FilterSecurityInterceptor之前,先要放行登录和验证请求,必须要return,使用SecurityContextHolder获取context上下文设置认证信息,这个认证信息需要UsernamepasswordAuthenticationToken对象利用构造方法创建,形参token中的载荷,null,authorities,其中的authorities通过AuthoritUtils中的commaSeparatedStringtoAuthorityList获取
- 前端发送请求时就需要将正确的token放到请求头中,后端验证之后方可访问资源,在vue的utils下的request.js中请求前从localstorage中获取token,然后设置到请求头中
- 此时如若token不正确或过期,无法访问资源,但依然会停留在访问页面,这样的用户体验不好,所以需要增加一个认证环节
- 后台认证:同样利用公钥解析请求头中的token,此过程无异常则表示token合法,正常返回一个ResultVO,需要注意的是:该认证的请求也需要在basicAuthenticationFilter和security配置类中放行
- 前端认证:在main.js中解开之前注释的permission组件,在permission.js中发送后台认证的请求,通过返回的数据判断token是否有效,有效则让原请求去访问资源,需要注意的是:要判断该请求是否是登录请求,若是登录请求可直接放行,访问登录页面,若token不合法就跳回登录页面
1. 登录认证,颁发令牌
- 直接使用vue的index.vue界面,改变其中username和password的绑定
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
<el-input
ref="username"
v-model="loginForm.userName"
placeholder="请输入用户名"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="请输入密码"
name="password"
tabindex="2"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
- 修改api,发送axious后端登录验证
import request from '@/utils/request'
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
- 后端controller层
@PostMapping("login")
@ApiOperation("用户登录")
public ResultVO login(@RequestBody SysUser sysUser) {
return userService.login(sysUser);
}
- 后端service层
@Override
public ResultVO login(SysUser loginUser) {
try {
if (loginUser == null) {
return new ResultVO(false, "用户名或密码不正确");
}
// 通过用户名查询用户是否存在,因为密码再数据库中是密文
SysUser userInDB = userMapper.findUserByUserName(loginUser.getUserName());
if (userInDB == null) {
// 用户不存在
return new ResultVO(false, "用户名或密码不正确");
}
// 用户存在,对比密码
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 前一个参数是明文密码,后一个是数据库中的密文密码
boolean matches = encoder.matches(loginUser.getPassword(), userInDB.getPassword());
if (!matches) {
// 密码不匹配
return new ResultVO(false, "用户名或密码不正确");
}
// 密码匹配,颁发令牌
// 1.获取用户的角色信息
List<String> rolesList = userMapper.findRolesByUserId(String.valueOf(userInDB.getUserId()));
String roles = "";
// 遍历集合
for (String roleName : rolesList) {
roles += roleName + ",";
}
roles = roles.substring(0, roles.length() - 1);
// 设置用户的角色信息
userInDB.setRoles(roles);
// 2.获取用户的权限信息
List<String> permissionList = userMapper.findPermissionByUserId(userInDB.getUserId());
String permissions = "";
// 遍历集合
for (String permissionName : permissionList) {
permissions += permissionName + ",";
}
permissions = permissions.substring(0, permissions.length() - 1);
// 设置用户的权限信息
userInDB.setPermissions(permissions);
// token载荷中不要放敏感信息
userInDB.setPassword("");
// 3.获取密钥
PrivateKey privateKey = RsaUtils.getPrivateKey(ResourceUtils.getFile("classpath:rsa_pri").getPath());
// 4. 颁发令牌
String token = JwtUtils.generateTokenExpireInMinutes(userInDB, privateKey, 45);
// 5. 没有异常正常返回
return new ResultVO(true, "登录成功", token);
} catch (Exception e) {
e.printStackTrace();
return new ResultVO(false, "令牌不合法!!!");
}
}
- 后端mapper层
// 通过用户名查询用户
@Select("select * from sys_user where user_name = #{userName}")
public SysUser findUserByUserName(String userName);
//查询用户的角色信息
@Select("SELECT role.role_name FROM sys_user_role sur " +
"LEFT JOIN sys_role role ON sur.role_id = role.role_code " +
"WHERE sur.user_id = #{userId}")
public List<String> findRolesByUserId(String userId);
// 查询用户权限信息
@Select("SELECT per.perm_name FROM sys_user_role sur \n" +
"LEFT JOIN sys_role_permission srp ON sur.role_id = srp.role_id\n" +
"LEFT JOIN sys_permission per ON srp.perm_id = per.perm_code\n" +
"WHERE sur.user_id = 1;")
public List<String> findPermissionByUserId(Integer userId);
2. 配置信息
- SpringSecurity的配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 放行静态资源
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/swagger-ui.html/**","/css/**","/images/**","/plugins/**","/webjars/**",
"/swagger-resources/**","/v2/**"
);
}
// 放行系统资源
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//登录请求必须放行
.antMatchers("/user/login","/user/verify").permitAll()
//除了以上资源,剩下的http资源都必须登录后才能访问
.anyRequest().authenticated();
//关闭ccrf过滤器
http.csrf().disable();
//解决跨域
http.cors();
http.addFilter(new MyBasicAuthenticationFilter(authenticationManager()));
}
}
- swagger2的配置,需要加入token
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("锋迷铺子运营平台接口文档")
.description("锋迷铺子运营平台接口文档")
.version("1.0.0")
.build();
}
@Bean
public Docket createRestApi(Environment environment) {
Profiles profiles = Profiles.of("dev");
boolean enable = environment.acceptsProfiles(profiles);
//添加head参数start
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enable)
.select()
.apis(RequestHandlerSelectors.basePackage("com.qf")) //这里写的是API接口所在的包位置
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars);
}
private static List<ApiKey> unifiedAuth() {
List<ApiKey> arrayList = new ArrayList();
arrayList.add(new ApiKey("Authorization", "token", "header"));
return arrayList;
}
}
注意: 此时在登录之后,后端会给前端颁发一个令牌,此后的请求中,前端必须要携带这个令牌进行访问,而后端也需要验证令牌的正确性才能决定是否可以放行访问资源,下面需要做的就是认证的操作
3. 认证:后端操作
- SpringSecurity中的BasicAuthenticationFilter就是做认证处理的
此过滤器在FilterSecurityInterceptor之前,所以需要先放行登录和认证的请求
此外,在认证之后需要将用户的信息,包括角色和权限都要放到springsecurity的上下文中,这样springsecurity才能起作用
public class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
// 因该过滤器在前面,所以要前放行login
String requestURI = request.getRequestURI();
if ("/user/login".equals(requestURI)) {
// 放行
chain.doFilter(request, response);
// 必须加return
return;
}
// 从请求头中获取jwt令牌 约定key为token
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
// 直接拦截,响应给客户端
response(response, new ResultVO(false, "令牌不能为空"));
}
// token不为空,利用公钥解密token获取用户信息
// 获取公钥
PublicKey publicKey = RsaUtils.getPublicKey(ResourceUtils.getFile("classpath:rsa_pub").getPath());
// 解密token
SysUser info = (SysUser) JwtUtils.getInfoFromToken(token, publicKey, SysUser.class);
// 获取用户的authorities
String authority = info.getRoles() + "," + info.getPermissions();
// 利用工具类获取
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(info, null, authorities);
// 将用户的信息放到spring security的上下文中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 放行
chain.doFilter(request, response);
} catch (Exception e) {
e.printStackTrace();
response(response,new ResultVO(false,"无效令牌!!!"));
}
}
private void response(HttpServletResponse response, ResultVO res) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
out.write(JSONUtil.toJsonPrettyStr(res));
out.flush();
out.close();
} catch (Exception e) {
}
}
}
- 认证的配置
// 放行系统资源
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//登录请求必须放行
.antMatchers("/user/login","/user/verify").permitAll()
//除了以上资源,剩下的http资源都必须登录后才能访问
.anyRequest().authenticated();
//关闭ccrf过滤器
http.csrf().disable();
//解决跨域
http.cors();
// 此为配置我们自己写的basicAuthenticationFilter过滤器
http.addFilter(new MyBasicAuthenticationFilter(authenticationManager()));
有了后端认证之后,前端发的请求中必须要携带正确的token才能访问资源
4.认证:前端操作
- 每次请求需要将token加入到请求头中,并按照约定的key去设置token
// 请求拦截器
service.interceptors.request.use(
config => {
// 将token放入请求头中
var tokenVal = localStorage.getItem('token')
config.headers.token = tokenVal
return config
},
error => {
return Promise.reject(error)
}
)
- 前端的请求实则是通过路由进行页面跳转,我们需要在permission.js中去修改拦截器,前提:在main.js中解开之前注释的permission组件
/**
* 在路由跳转之前,要将token放到请求头中,后台才能做验证
*/
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
verify().then(res => {
if (!res.success) {
if (to.path === '/login') {
next()
} else {
// 跳回登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
// 放行
next()
}
})
})