0、什么是jwt,什么是Security
两篇完成项目主要需要的基础知识
jwt是什么?:https://www.cnblogs.com/yan7/p/7857833.html
security登录的过程:https://blog.csdn.net/abcwanglinyong/article/details/80981389
1、导包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.1.0.RELEASE</spring-boot.version>
<akka.version>2.5.21</akka.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>com.kejin.util</groupId>
<artifactId>autoCoding</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.6</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-autoconfigure</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
基本上用到的包在都了。
2、创建一个springBoot工程,使用idea的spring initializr.然后导包
说一下实现思路
第一次登录:用户登录----security拦截-----根据用户名查信息是否存在-----存在放入缓存----生成token-----返回客户端
第二次访问:携带token—缓存取值–存在就放行。
要实现:
UsernamePasswordAuthenticationFilter拦截器执行前添加一个自定义拦截器(JwtAuthorizationTokenFilter)。
并且写一个拦截器(LoginFilter)替换UsernamePasswordAuthenticationFilter拦截器。
JwtAuthorizationTokenFilter用于验证token信息(是否过期等等),并将其存在security的上下文中(及当前用户信息)SecurityContextHolder.getContext().setAuthentication(这里存用户信息)
0、贴上JwtUtil工具类
package com.top.system.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.UUID;
/**
* @author 网上搜索的
* @desc JWT工具类
**/
public class JwtUtil {
/**
* 一周过期时间
*/
private static final long EXPIRE_TIME =7 * 24 * 3600 * 1000;
/**
* 校验token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
//根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
//效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getVersion(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("version").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String version, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withClaim("version", version)
.withExpiresAt(date)
.withClaim("random", UUID.randomUUID().toString())
.sign(algorithm);
}
}
1、新建JwtAuthorizationTokenFilter 继承BasicAuthenticationFilter(基础权限过滤)
/**
* @author 薛向毅
* @description JWT token认证过滤器
*/
@Slf4j
public class JwtAuthorizationTokenFilter extends BasicAuthenticationFilter {
@Autowired
private Cache<String, JwtUser> cache;
@Autowired
public JwtAuthorizationTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 校验token合法性,然后组装成认证实体给后续的组件{@link MyPermissionEvaluator}判断权限
*MyPermissionEvaluator Security的自定义方法拦截类,他可以获取SecurityContextHolder.getContext().setAuthentication(jwtUser);存Authentication
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("token");
//若是登录的请求,就放行。
if (request.getRequestURI().equals("/login")) {
//登录url
chain.doFilter(request, response);
return;
}
if(request.getRequestURI().equals("/")) {
return;
}
if (StringUtil.isEmpty(token)) {
//没有token直接放行。 在securityConfig配置了认证,故 SecurityContextHolder.getContext().setAuthentication();没设置值会抛出认证异常
chain.doFilter(request, response);
return;
}
//下面的方法。根据token获取当前登录用户信息
JwtUser jwtUser = buildAuthentication(token);
//这里存储后的jwUser包含了用户的所有权限信息。 ***********即登陆后访问其他资源
SecurityContextHolder.getContext().setAuthentication(jwtUser);
chain.doFilter(request, response);
}
/**
* 解析token 利用jwt解密并且本地缓存中存在才算具有凭证
* <p>
* 抛出异常或者返回空最终都会走到 {@link JwtAuthenticationEntryPoint#commence(
*javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.AuthenticationException)}方法
*
* @param token
* @return
*/
private JwtUser buildAuthentication(String token) {
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new BadCredentialsException("token无效!");
}
//缓存中取出用户信息
JwtUser jwtUser = cache.getIfPresent(username);
if (jwtUser == null || !jwtUser.getVersion().equals(JwtUtil.getVersion(token))) {
throw new BadCredentialsException("token无效!");
}
if (!jwtUser.getEnable()) {
throw new DisabledException("该账号已被禁用!");
}
return jwtUser;
}
}
2、先根据上面的拦截。/login 若为登录请求的话。创建LoginFilter
package com.top.system.security.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.top.system.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.RequestUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 薛向毅
* @description 自定义登录拦截。重写了方法,只能使用json字符串方式登录。后面会添加进security拦截器链
*/
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
if ("application/json".equalsIgnoreCase(request.getHeader("Content-Type"))) {
String json = HttpUtils.parseJsonContent(request);
if (StringUtils.isNotBlank(json)) {
JSONObject jsonObj = JSON.parseObject(json);
if (jsonObj == null) {
return null;
}
String username = jsonObj.getString(super.getUsernameParameter());
String password = jsonObj.getString(super.getPasswordParameter());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
//这里是个重点 会调用适合的privoder里面的authenticate()方法。
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} else {
return null;
}
} else {
return null;
}
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (request.getRequestURI().equals("/login")) {
return true;
}
return false;
}
}
3、根据security的介绍。他会调用合适的provider执行authenticate来验证账号密码
package com.top.system.security.provider;
import com.qinwell.common.util.StringUtil;
import com.top.system.security.handler.AuthenticationSuccessHandler;
import com.top.system.security.model.AdminDetails;
import com.top.system.service.AdminRoleService;
import com.top.system.security.model.JwtUser;
import com.top.system.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @author 薛向毅
* @description 登录业务逻辑
*/
@Service
public class LoginProvider implements AuthenticationProvider {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private AdminRoleService adminRoleService;
@Autowired
private PasswordEncoder bCryptPasswordEncoder;
/**
* 登录验证
* 成功后返回值转发到{@link AuthenticationSuccessHandler#onAuthenticationSuccess
* (javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
* org.springframework.security.core.Authentication)}
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
if (StringUtil.isEmpty(username)) {
throw new BadCredentialsException("用户名不能为空!");
}
//自定义账号详情 AdminDetails 。 jwtUserDetailsService是定义实现了UserDetailsService的
AdminDetails adminDetails = (AdminDetails) jwtUserDetailsService.loadUserByUsername(username);
if (!bCryptPasswordEncoder.matches(authentication.getCredentials().toString(), adminDetails.getPassword())) {
throw new BadCredentialsException("密码错误!");
}
if (!adminDetails.getEnable()) {
throw new DisabledException("该账户已被禁用!");
}
//这里会返回登录成功的用户信息
return new JwtUser(adminDetails.getUsername(), adminDetails.getUsername(),
adminDetails.getAuthorities(), adminDetails.getAdmin(), adminDetails.getPermissionVos(), adminDetails.getEnable());
}
//**这个方法就代表了 合适!!!! 自定义让他返回true。故会调用这个类进行登录验证
@Override
public boolean supports(Class<?> paramClass) {
if (paramClass.isAssignableFrom(UsernamePasswordAuthenticationToken.class)) {
return true;
}
return false;
}
}
4、继承UserDetailsService重写loadUserByUsername方法
/**
*/
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private AdminRoleMapper adminRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private Cache cache;
/**
* 根据用户名获取用户信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
//查询账户基本信息
Admin admin = adminMapper.selectOne(queryWrapper);
if (admin == null) {
throw new UsernameNotFoundException("未找到该用户!");
}
//查权限啊 查角色啊 你想查什么查什么,放在自定义的adminDetails 然后return出去
AdminDetails adminDetails = new AdminDetails(admin.getUsername(), admin.getPassword(), authorities, enable);
//存放菜单
adminDetails.setPermissionVos(permissionVos);
adminDetails.setAdmin(admin);
adminDetails.setRoles(roles);
return adminDetails;
}
/**
* 清除指定用户缓存
* @param username
*/
public void clearCache(String username) {
cache.invalidate(username);
}
}
5、贴上UserDetail类
/**
* User是Security里面的User 他实现了UserDetails 可以存储账号,密码 ,权限信息(字符串一般情况)
*/
@Data
public class AdminDetails extends User {
private Admin admin;
private List<PermissionVo> permissionVos;
private List<Integer> roles;
private Boolean enable;
private AdminDetails() {
super(null,null,null);
}
public AdminDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Boolean enable) {
super(username, password, authorities);
this.enable = enable;
}
}
6、根据security特性,登陆成功后会调用AuthenticationSuccessHandler。我们重写他
/**
SavedRequestAwareAuthenticationSuccessHandler 最终是实现了这个接口AuthenticationSuccessHandler
*/
@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private Cache cache;
/**
* 登录成功生成jwt,并将认证信息放入缓存,具有可控性
*
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override //一旦登录成功,调用。 讲token返回给前台页面。(无论谁登录都要携带token ,在header中。验证)
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//日期当作版本
String version = new Date().toString();
//生成jwt 这里密码也用用户名填写,避免不安全。这个token是为了验证登录
String token = JwtUtil.sign(authentication.getPrincipal().toString(),
version, authentication.getPrincipal().toString());
//token中的version和缓存中的version一致才代表缓存有效
JwtUser jwtUser = (JwtUser) authentication;
jwtUser.setVersion(version);
//放入缓存 key用户名, 用户信息
cache.put(authentication.getPrincipal().toString(), jwtUser);
//这个方法就是response的write 把token返回前端
ResponseUtil.out(response, JSON.toJSONString(R.ok(token)));
}
}
7到目前回执,jwt登录已经完成。登录验证也完成。 下面进行方法过滤
url拦截在别的博客也写过,如果进行简单的方法过滤,可以使用自定义PermissionEvaluator
@Service
public class MyPermissionEvaluator implements PermissionEvaluator {
//方法上面使用@PreAuthorize(" hasPermission('or', 'role_list')") 即可拦截
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
return hasPermission(authentication, null, (String) targetDomainObject, permission);
}
//第一个参数登陆后生成,上下文用户信息。 第三个 即传递的or 第四个传递的过滤权限信息
//这里 取出 用户拥有的权限编码(因为如果用角色的话他的权限可改变,权限编码一个编码一个功能点,不可变)
// 对比 用户是否拥有编码信息 有就放行,没有就给出权限不足
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if ("OR".equalsIgnoreCase(targetType)) {
String[] split = permission.toString().split(",");
for (String s : split) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (s.equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
return false;
} else {
String[] split = permission.toString().split(",");
for (String s : split) {
boolean isMatch = false;
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (s.equals(grantedAuthority.getAuthority())) {
isMatch = true;
break;
}
}
if (!isMatch) {
return false;
}
}
return true;
}
}
}
8、开启MyPermissionEvaluator
@EnableGlobalMethodSecurity(prePostEnabled = true) //prePostEnabled 代表可以使用@PreAuthorize等注解,可以去官方文档查看
public class WebMvcConfigurationSupport extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return defaultMethodSecurityExpressionHandler();
}
@Bean
public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
defaultMethodSecurityExpressionHandler.setPermissionEvaluator(myPermissionEvaluator());
return defaultMethodSecurityExpressionHandler;
}
/**
* 使用hasPermission自定义验证
*/
@Bean
public MyPermissionEvaluator myPermissionEvaluator() {
MyPermissionEvaluator myPermissionEvaluator = new MyPermissionEvaluator();
return myPermissionEvaluator;
}
}
9重要的security配置类。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//auth.authenticationProvider(myAuthenticationProvider);
}
@Bean
public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter() throws Exception {
JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter = new JwtAuthorizationTokenFilter(authenticationManager());
return jwtAuthorizationTokenFilter;
}
@Bean
public LoginFilter loginFilter(AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationManager(authenticationManager());
loginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
loginFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
return loginFilter;
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
public Cache cache() {
return CacheBuilder.newBuilder().expireAfterAccess(3, TimeUnit.DAYS).build();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用 CSRF
.csrf().disable()
// 授权异常
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).
accessDeniedHandler(customAccessDeniedHandler).and()
// 不创建会话
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
.antMatchers(
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
//"/error"
).permitAll()
//.antMatchers(HttpMethod.POST, "/auth/" + loginPath).permitAll()
.antMatchers("/websocket/**").permitAll()
// swagger start
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
// swagger end
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
// 所有请求都需要认证
.anyRequest().authenticated();
httpSecurity.formLogin()
.loginProcessingUrl("/login")
.permitAll();
LoginFilter loginFilter = loginFilter(null, null);
httpSecurity.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(loginFilter);
}
//@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
/**
* 跨域
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
10 为什么叫单点登录、。
使用springcloud分布式开发,将这个项目使用Feign远程调用。 每次请求去缓存里面看客户端携带的token是否正确即可
有问题直接留言,看到就会解答。比较懒 不喜欢扣字。所以代码多