在初次接触java的web开发时对整个框架都不太了解,这里提供一下比较流行的前后端完全分离的开发模式vue\security和jwt如何集成的问题(太多朋友发的解决办法security的config配置都涉及到自己定制登录页面提交请求,这样的话后端代码必然需要一个登录页面比如login.html或者login.vue,那大部分都是视屏讲课老师在授课时为了方便定制前端页面,而且在认证后会自动有个跳转页面真正在前后端分离时后端并没有前端的页面,仅仅提供接口返回数据)
在集成到项目时主要有几个重要的步骤:
第一步:
流程理解
从数据源(这里是数据库)中获取用户信息组装到 UserDetails, 然后通过UserDetailsService(重写loadUserByName方法),传递 UserDetails; SecurityContextHolder 存储 整个 用户上下文信息,通过SecurityContext 存储 Authentication, 这样就保证了 springSecurity 持有用户信息;
注意:认证时security认证核心authenticationManager()方法会默认调用UserDetailsService.loadUserByName方法从而进行登录名验证(重写了即可验证数据库信息)
第二步:
添加maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
除此之外fastjson等等依赖自行添加
第三步:
jwtUserdto实现 UserDetails 用于储存用户信息, 主要是用户名,密码, 和权限;
@Data
@NoArgsConstructor
public class JwtUserDto implements UserDetails {
/**
* 用户数据
*/
private MyUser myUser;
private List<MyRole> roleInfo;
/**
* 用户权限的集合
*/
@JsonIgnore
private List<GrantedAuthority> authorities;
public List<String> getRoles() {
return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
}
/**
* 加密后的密码
* @return
*/
@Override
public String getPassword() {
return myUser.getPassword();
}
/**
* 用户名
* @return
*/
@Override
public String getUsername() {
return myUser.getUserName();
}
/**
* 是否过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 凭证是否过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用
* @return
*/
@Override
public boolean isEnabled() {
return myUser.getStatus() == 1 ? true : false;
}
public JwtUserDto(MyUser myUser, List<GrantedAuthority> authorities) {
this.myUser = myUser;
this.authorities = authorities;
}
}
第四步
token 工具类主要用于生产 token, 解析token, 校验token;这边需要注意的是,将 权限 归并到了生成 toekn 的步骤,这样通过 token就可以获取 权限,在权限校验时通过token就可以获取权限信息(后续还需要跟新token过期刷新问题,权限同步刷新的问题)
@Component
public class JwtUtils {
private static final String CLAIMS_ROLE = "MyUserRoles";
//定义token过期时间两天
private static final long EXPIRATION_TIME = 24* 60 * 60 * 2;
//自定义JWT密码,不得泄漏(我乱写出来给看的)
private static final String SECRET = "mySecretgjkfafgafjjkaf";
//签发JWT
public static String getToken(String username, String roles) {
Map<String, Object> claims = new HashMap<>(8);
// 主体
claims.put(CLAIMS_ROLE, roles);
return Jwts.builder()
.setClaims(claims)
.claim("username", username)
.setExpiration(new Date(Instant.now().toEpochMilli() + EXPIRATION_TIME))// 过期时间
.signWith(SignatureAlgorithm.HS512, SECRET)// 加密策略
.compact();
}
/**
* 验证JWT//(还需要加上用户的details)
*/
public static Boolean validateToken(String token) {
return (!isTokenExpired(token));
}
/**
* 验证token是否过期
*/
public static Boolean isTokenExpired(String token) {
Date expiration = getExpireTime(token);
return expiration.before(new Date());
}
/**
* 根据token获取username
*/
public static String getUsernameByToken(String token) {
String username = (String) parseToken(token).get("username");
return username;
}
/**
* 在每次请求时,先拿到jwt令牌解析获得其中的权限让用户保持登录
* @param token
* @return
*/
public static Set<GrantedAuthority> getRolseByToken(String token) {
String rolse = (String) parseToken(token).get(CLAIMS_ROLE);
String[] strArray = StringUtils.strip(rolse, "[]").split(", ");
Set<GrantedAuthority> authoritiesSet = new HashSet();
if (strArray.length > 0) {
Arrays.stream(strArray).forEach(rols -> {
GrantedAuthority authority = new SimpleGrantedAuthority(rols);
authoritiesSet.add(authority);
});
}
return authoritiesSet;
}
/**
* 获取token的过期时间
*/
public static Date getExpireTime(String token) {
Date expiration = parseToken(token).getExpiration();
return expiration;
}
/**
* 解析JWT
*/
private static Claims parseToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return claims;
}
}
第五步 实现UserDetailsService 类
UserDetailsService 用户查询数据库的数据信息,进行用户数据封装到UserDetails, 在进行用户身份认证的时候会走这边; 这边采用官方提供的PasswordEncoder 进行加密; 其配置方式需要在WebSecurityConfig 中 配置;
笔者这里重写loadUserByUsername方法并设置从数据库拿到的权限
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleUserService roleUserService;
@Autowired
private RoleService roleService;
@Autowired
private MenuDao menuDao;
@Override
public JwtUserDto loadUserByUsername(String userName) {
// 根据用户名获取用户
MyUser user = userService.getUserByName(userName);
if (user == null) {
throw new BadCredentialsException("用户名或密码错误");
} else if (user.getStatus().equals(MyUser.Status.LOCKED)) {
throw new LockedException("用户被锁定,请联系管理员解锁");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<String> collect = new ArrayList<>();
List<MenuIndexDto> list = menuDao.listByUserId(user.getUserId());
for (int i = 0; i < list.size(); i++) {
if(list.get(i)!=null){
if (list.get(i).getPermission() != null)
collect.add(list.get(i).getPermission());
else{
//啥也不知道啊
}
// List<String> collect = list.stream().map(MenuIndexDto::getPermission).collect(Collectors.toList());
}
}
for (String authority : collect) {
if (!("").equals(authority) & authority != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority);
grantedAuthorities.add(grantedAuthority);
}
}
//将用户所拥有的权限加入GrantedAuthority集合中
JwtUserDto loginUser = new JwtUserDto(user, grantedAuthorities);
loginUser.setRoleInfo(getRoleInfo(user));
return loginUser;
}
public List<MyRole> getRoleInfo(MyUser myUser){
MyUser userByName=userService.getUserByName(myUser.getUserName());
List<MyRoleUser> roleUserByUserId=roleUserService.getMyRoleUserByUserId(userByName.getUserId());
List<MyRole> roleList=new ArrayList<>();
for(MyRoleUser roleUser:roleUserByUserId){
Integer roleId=roleUser.getRoleId();
MyRole roleById=roleService.getRoleById(roleId);
roleList.add(roleById);
}
return roleList;
}
}
第六步 JWTLoginFilter过滤器
JWTLoginFilter 继承 AbstractAuthenticationProcessingFilter 过滤器
JWTLoginFilter 用于用户登陆认证,其实现如下 三个方法 ;
请注意这个过滤器只会拦截"/login"请求,即登录请求,成功则颁布令牌
attemptAuthentication 用于 尝试认证,如果认证成功会走 successfulAuthentication 方法;如果认证失败会走 unsuccessfulAuthentication 方法;
successfulAuthentication 认证成功后我们需要生成一个token,返回以JSON的形式返回给前端;
unsuccessfulAuthentication 认证失败,我们通过异常信息判定,然后返回错误信息给前端;
这里着重说明下 attemptAuthentication方法会接受前端的用户名密码
该方法返回值 return getAuthenticationManager().authenticate(authenticationToken)中getAuthenticationManager()会默认调用第五步中实现类UserDetailsServiceImpl中重写的loadUserByUsername方法去验证用户名是否合法并给与权限(用户密码这里没有验证,密码验证在后面)
//登陆认证过滤器
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
//登录认证
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String s1=request.getParameter("username");
String s2=request.getParameter("password");
// JwtUserDto user = new ObjectMapper().readValue(request.getInputStream(), JwtUserDto.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(s1, s2);
return getAuthenticationManager().authenticate(authenticationToken);
}
//登录成功回调
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth){
JwtUserDto principal = (JwtUserDto)auth.getPrincipal();
String token = JwtUtils.getToken(principal.getUsername(),principal.getAuthorities().toString());
try {
//登录成功時,返回json格式进行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
//登录成功返回策略.将用户的token返回给前台
ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,token);
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
//登录失败策略
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String result="";
if (failed instanceof BadCredentialsException) {
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(Result.error().message("密码错误")));
}
else {
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(Result.forbiddenError().message("登录信息异常!")));
}
}
}
第七步 JwtAuthenticationFilter 过滤器
继承BasicAuthenticationFilter过滤器(是OncePerRequestFilter的子类)
注意下述为空表明用户未登录,一次过滤后security认证会自动调用方法清空
SecurityContextHolder.getContext().getAuthentication() == null
因此再次请求就要先将用户信息交由security处理
@Component
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
//此处暂时略过
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头中取出 token
String token = request.getHeader("token");
// 2. 判断token是否为空
if (StringUtils.isNotBlank(token)) {
// 3. 从 token 里获取用户名
String username = JwtUtils.getUsernameByToken(token);
// 4. 只要 token 没过期,就让用户保持登录状态 jwt 令牌只是辅助登录, 真正是否登录要看 authentication 是否有效
if (StringUtils.isNotBlank(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
// token校验,保持登陆,此处表明令牌有效—————》是有效的登录用户
if (JwtUtils.validateToken(token)) {
Set rolseByToken = JwtUtils.getRolseByToken(token);
Authentication authentication = new UsernamePasswordAuthenticationToken(username,null, rolseByToken);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
第八步WebSecurityConfig配置
WebSecurityConfig 是 springSecurity 的配置相关信息;在配置中,可以进行数据访问权限限制,授权异常处理,账号加密方式等配置;前文提及的用户密码验证在这里身份认证接口
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())将userDetailsService注入即可验证密码是否正确
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DenyHandler denyHandler;
@Autowired
OutSuccessHandler outSuccessHandler;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
ExpAuthenticationEntryPoint expAuthenticationEntryPoint;
// /**
// * 验证码拦截器
// */暂时没做验证码
// @Autowired
// private VerifyCodeFilter verifyCodeFilter;
@Override
public void configure(WebSecurity web) throws Exception {
//放行静态资源
web.ignoring().antMatchers(HttpMethod.GET,
"/swagger-resources/**",
"/PearAdmin/**",
"/component/**",
"/admin/**",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-ui.html",
"/webjars/**",
"/v2/**",
"/druid/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
//关闭csrf
http.csrf().disable()
// .sessionManagement()// 基于token,所以不需要session
// .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
//未登陆时返回 JSON 格式的数据给前端
.authorizeRequests()
//任何人都能访问这个请求
.and()
.authorizeRequests()
.antMatchers("/login","/api/login").permitAll()
// .antMatchers("/","/pageHome").permitAll()
// .antMatchers("/captcha").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(denyHandler)//授权异常处理
.authenticationEntryPoint(expAuthenticationEntryPoint)// 认证异常处理
.and()
.logout()
.logoutSuccessHandler(outSuccessHandler)
.and()
//只会拦截登录的请求
.addFilterBefore(new JWTLoginFilter("/api/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class)
//拦截登录请求成功后,持有令牌。则如下过滤可通过
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class)
.sessionManagement()
.and()
.rememberMe().rememberMeParameter("rememberme")
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()
.and();
// 禁用缓存
http.headers().cacheControl();
}
//利用密码编码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
第九步Handeler配置
配置中使用到了3个处理类,分别是 denyHandler, outSuccessHandler, expAuthenticationEntryPoint;
其中 denyHandler 当权限进行校验时,如果权限不足就会走这个处理类
@Component
public class DenyHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 设置响应头
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.error(CodeMsg.PERM_ERROR);
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
outSuccessHandler 是退出登陆处理类,默认地址 localhost:8080/logout;
@Component
public class OutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 设置响应头
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.sucess(CodeMsg.SUCESS,"退出登陆成功");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
expAuthenticationEntryPoint 负责身份认证通过后异常处理,每个主要身份验证系统都有自己的AuthenticationEntryPoint实现;
//处理匿名请求拦截用户让其登录
@Component
public class ExpAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置响应头
httpServletResponse.setContentType("application/json;charset=utf-8");
// 返回值
ResultPage result = ResultPage.error(CodeMsg.ACCOUNT_ERROR);
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
第十步 Controller
前端的每次请求都会携带jwt令牌(token在请求头)
拦截器拦截后判别身份若为合法则看是否有权限条件满足则会接受请求
@PreAuthorize(“hasAnyAuthority(‘user:list’)”)注解是请求前鉴权(权限在数据库中,重写的loadUserByName方法会从数据库读取)
@Controller
@RequestMapping("/api/user")
@Api(tags = "系统:用户管理")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JobService jobService;
@Autowired
private DeptService deptService;
@GetMapping
@ResponseBody
@ApiOperation(value = "用户列表")
@PreAuthorize("hasAnyAuthority('user:list')")
@MyLog("查询用户")
public Result<MyUser> userList(PageTableRequest pageTableRequest, MyUser myUser){
pageTableRequest.countOffset();
return userService.getAllUsersByPage(pageTableRequest.getOffset(),pageTableRequest.getLimit(),myUser);
。。。。。。。。。。。
}
}
其实security+jwt的方式很多,只要拦截器合理配置就行(基佬网站有类似源码可自行查阅)