注:使用前后端分离.
1.导入需要的springboot的pom依赖文件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.18version>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.5.4version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
<version>3.0-alpha-1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.1.0version>
dependency>
我在保存密码中使用的md5加密所以需要创建工具类
MD5Util工具类:
private static final String SALT = "123456789";
//生成md5加密后的密码
public static String encode(String password){
String s = addSalt(password, SALT);
return DigestUtils.md5DigestAsHex(s.getBytes());
}
//将salt于密码进行拼接,让密码更加复杂
private static String addSalt(String password,String salt){
return salt.charAt(0)+password+salt;
}
创建jedispool:
# Redis服务器地址
redis.host=127.0.0.1
# Redis服务器连接端口
redis.port=6379
# Redis服务器连接密码(默认为空)
redis.password=
# 连接超时时间(毫秒)
redis.timeout=10000
# 连接池最大连接数(使用负值表示没有限制)
redis.pool.maxActive=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
redis.pool.maxWait=-1
# 连接池中的最大空闲连接
redis.pool.minIdle=8
# 连接池中的最小空闲连接
redis.pool.maxIdle=0
@Configuration
@PropertySource("classpath:jedisConfig.properties")
public class JedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private Integer port;
@Value("${redis.password}")
private String password ;
@Value("${redis.pool.maxIdle}")
private int maxIdle;
@Value("${redis.pool.maxActive}")
private int maxActive;
@Value("${redis.pool.maxWait}")
private int maxWait;
@Value("${redis.pool.minIdle}")
private int minIdle;
@Value("${redis.timeout}")
private int timeout ;
@Bean
public JedisPool pool(){
try {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWait);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
// 密码为空设置为null
if (StringUtil.isEmpty(password)) {
password = null;
}
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
if (jedisPool==null){
System.out.println("初始化Redis连接池JedisPool失败!地址: " + host + ":" + port);
}
System.out.println("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port);
return jedisPool;
} catch (Exception e) {
System.out.println("初始化Redis连接池JedisPool异常:" + e.getMessage());
}
return null;
}
}
创建jedisUtil用于redis的连接
@Component
public class JedisUtil {
private static JedisPool jedisPool;
@Autowired
public void setJedisPool(JedisPool jedisPool) {
JedisUtil.jedisPool = jedisPool;
}
//添加
public static boolean set(String key ,String value,Integer timeout){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String s = jedis.set(key, value);
if (timeout!=null){
jedis.expire(key,timeout);
}
if (s.equals("ok")){
return false;
}
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}finally{
jedis.close();
}
}
//删除
public static boolean del(String key ){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(key);
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}finally{
jedis.close();
}
}
//判断是否存在
public static boolean isExist(String key ){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.exists(key);
}catch(Exception e){
e.printStackTrace();
return false;
}finally{
jedis.close();
}
}
//获取信息
public static String get(String key){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.get(key);
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
jedis.close();
}
}
//获取超时时间
public static long getTimeOut(String key){
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.ttl(key);
}catch(Exception e){
e.printStackTrace();
return -1;
}finally{
jedis.close();
}
}
}
创建生成jwt的工具类:
public class JwtTokenUtil {
//秘钥..可以自己定义.需要复杂,简单会报错
private static final String SECRET= "11111111111";
//过期时间 单位毫秒
private static final long EXPSECOND = 60*1000*24*60*7;
//刷新令牌时间 单位秒
private static final int REFRESHSECOND = 60*60*24*7;
private JwtTokenUtil(){}
//生成jwt
public static String encode(String username){
long start = System.currentTimeMillis();
JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
.claim("username",username)
.setExpiration(new Date(start + EXPSECOND))
.signWith(SignatureAlgorithm.HS256, SECRET);
return jwtBuilder.compact();
}
//jwt过期后重新生成jwt
public static String refreshToken(String jwt){
Claims claims = decode(jwt);
String username = (String)claims.get("username");
return encode(username);
}
//解码jwt
public static Claims decode(String jwt){
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwt).getBody();
}catch(ExpiredJwtException e){
e.printStackTrace();
claims = e.getClaims();
}finally{
return claims;
}
}
//获取令牌在redis中过期时间
public static int getRefreshSecond() {
return REFRESHSECOND;
}
//前端发送的token可能有Bearer前缀,解析前需要删除
public static String delBearer(String token){
return token.replace("Bearer","").trim();
}
//添加Bearer前缀
public static String addBearer(String token){
return "Bearer "+token;
}
//判断是否过期,过期解析会抛出异常,返回true
public static boolean isExpired(String token){
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
return false;
}catch(ExpiredJwtException e){
return true;
}
}
}
public class JwtToken implements AuthenticationToken {
private String token;
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
public JwtToken(String string){
this.token = string;
}
}
用于身份的验证
public class UserRealm implements Realm {
@Override
public String getName() {
return "userRealm";
}
//判断token是否实现authenticationToken,必须,不然报错
@Override
public boolean supports(AuthenticationToken authenticationToken) {
return authenticationToken instanceof JwtToken;
}
//验证用户身份
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
if (JwtTokenUtil.isExpired(JwtTokenUtil.delBearer(token))){
throw new ExpiredJwtException(null,null,null,null);
}
Claims claim = JwtTokenUtil.decode(JwtTokenUtil.delBearer(token));
String username = (String)claim.get("username");
if (JedisUtil.get(Constant.TOKEN+username).equals(token)){
return new SimpleAuthenticationInfo(token,token,getName());
}
throw new AuthenticationException("失败");
}
}
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//获取请求头中的token
String header = getAuthzHeader(request);
JwtToken token = new JwtToken(header);
//提交到realm验证
this.getSubject(request,response).login(token);
//报错不会返回true
return true;
}
//判断是否登录,通过是否携带token
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
String token = getAuthzHeader(request);
return !StringUtil.isEmpty(token);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
this.sendChallenge(request, response);
return false;
}
//支持跨域
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 查看当前Header中是否携带Authorization属性(Token),有的话就进行登录认证授权
if (this.isLoginAttempt(request, response)) {
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
return true;
} catch (Exception e) {
return false;
}
}else{
this.response401(response,"未携带Token");
return false;
}
}
/**
* 无需转发,直接返回Response信息
*/
private void response401(ServletResponse response, String msg){
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
try (PrintWriter out = httpServletResponse.getWriter()) {
out.append(msg);
} catch (IOException e) {
throw new BaseException("直接返回Response信息出现IOException异常:" + e.getMessage());
}
}
}
注:UserRealm不能通过注入方式生成,必须通过new,不然filterChainDefinitionMap的顺序将失效
造成无法拦截的情况
public class ShiroConfig {
/**
* 配置使用自定义Realm,关闭Shiro自带的session
*/
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Bean("securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 使用自定义Realm
defaultWebSecurityManager.setRealm(new UserRealm());
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器取名为jwt
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 自定义url规则使用LinkedHashMap有序Map
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(16);
//anon为匿名访问
filterChainDefinitionMap.put("/anon/**", "anon");
filterChainDefinitionMap.put("/user/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
有些请求需要使用用户数据,我们将从token中获取用户数据,解析后保存到request中,方便使用
public class tokenHeaderInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String header = request.getHeader("Authorization");
String token = JwtTokenUtil.delBearer(header);
Claims claim = JwtTokenUtil.decode(token);
String username = (String)claim.get("username");
if (!StringUtil.isEmpty(username)){
request.setAttribute("username",username);
}
response.setContentType("application/json;charset=utf-8");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
return true;
//包含Beare开头
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
response.setCharacterEncoding("UTF-8");
}
}
对需要使用用户信息的路径将其添加到配置文件中
public class WebConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
LinkedList<String> list = new LinkedList<>();
list.add("/user/login");
list.add("/user/register");
System.out.println("llllllllllllllll");
registry.addInterceptor(new tokenHeaderInterceptor()).addPathPatterns("/user/**").excludePathPatterns(list);
}
//跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(false).maxAge(3600);
}
}
@RestController
@RequestMapping("/anon")
public class CommonController {
@Autowired
private UserService userService;
@Autowired
private JedisUtil jedisUtil;
@RequestMapping("/login")
public Result login(@RequestBody Map map,HttpServletRequest req){
//通过Interceptor后,username将储存在req中
String username = (String)map.get("username");
//加密密码
String password = MD5Util.encode(((String)map.get("password")).trim());
//通过username查询数据库密码
User user = userService.selectUserByUsername(username);
if (user ==null){
return new Result(HttpStatus.EXPECTATION_FAILED.value(),"用户不存在",null);
}
if (user.getPassword().equals(password)){
String token = JwtTokenUtil.addBearer((JwtTokenUtil.encode(username)));
jedisUtil.set(Constant.TOKEN+username,token, JwtTokenUtil.getRefreshSecond());
return new Result(HttpStatus.OK.value(),"登录成功",token);
}
return new Result(HttpStatus.UNAUTHORIZED.value(),"未找到用户信息",null);
}
}
我们将使用postman进行验证
1.登录
返回结果:
在data中返回了jwt的字符串,表示成功.
2.不携带token访问需要登录的路径
可以看见返回未携带token的信息.
3.携带token访问需要登录的路径
添加token
结果: