美文网首页
搭建Json服务器并实现鉴权

搭建Json服务器并实现鉴权

作者: 十年磨剑的简书 | 来源:发表于2019-10-05 16:12 被阅读0次

项目代码托管在GitHub上.
有关登录操作的部分实现起来坑很多,爬了一个多礼拜才整出来,记一下,怕忘了.
首先用spring-mvc搞一个json服务器,这里仍然使用spring4框架,忽略这一阵很火但满是bug的spring-boot.利用gradle创建一个web工程,考虑到后续可能增加的业务,这里将工程设计为root模块和core/...子模块的结构,只通过core向外暴露接口,其它模块打成jar包引入到core中,实际工程目录结构如下:

目录结构
这里需要注意的是,2019版本以后,idea跟gradle耦合的很紧,在创建工程前不允许使用本地gradle,而直接从国外网站下载,需要眼疾手快地修改为本地版本.另外还要将打包方式改为idea自带,否则会有一些莫名其妙的bug.
gradle设置
不要忘了设置settings.gradle和build.gradle
//指定父子关系
rootProject.name = 'web'
include 'core'

// root下所有子项目的通用配置
subprojects {
    apply plugin: 'idea'

    group 'com.share'
    version '1.0-SNAPSHOT'

}

//core中引入项目依赖
plugins {
    id 'java'
    id 'war'
}

group 'com.share.web'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    maven {
        url "http://maven.aliyun.com/nexus/content/groups/public/"
    }
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    // https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core
    testCompile group: 'org.mybatis.generator', name: 'mybatis-generator-core', version: '1.3.7'

    // https://mvnrepository.com/artifact/org.springframework/spring-webmvc
    compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.25.RELEASE'
    // https://mvnrepository.com/artifact/com.alibaba/fastjson
    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.47'
    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.9'
    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.9'
    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9'

    // https://mvnrepository.com/artifact/mysql/mysql-connector-java
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.17'
    // https://mvnrepository.com/artifact/org.mybatis/mybatis
    compile group: 'org.mybatis', name: 'mybatis', version: '3.5.1'
    // https://mvnrepository.com/artifact/org.mybatis/mybatis-spring
    compile group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.1'
    // https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2
    compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.6.0'
    // https://mvnrepository.com/artifact/org.springframework/spring-jdbc
    compile group: 'org.springframework', name: 'spring-jdbc', version: '4.3.25.RELEASE'
    // https://mvnrepository.com/artifact/redis.clients/jedis
    compile group: 'redis.clients', name: 'jedis', version: '2.9.3'
    // https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis
    compile group: 'org.springframework.data', name: 'spring-data-redis', version: '1.8.23.RELEASE'
    // https://mvnrepository.com/artifact/com.auth0/java-jwt
    compile group: 'com.auth0', name: 'java-jwt', version: '3.8.1'
    // https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
    providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
    // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'

    // https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2
    compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.0.17.RELEASE'
    // https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt
    compile group: 'org.springframework.security', name: 'spring-security-jwt', version: '1.0.10.RELEASE'
    // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
    compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
    // https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
    compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
}

这里需要注意spring-security与spring版本间的匹配问题,以及补充Java8之后不再默认提供的xml.bind包.近年十分流行纯注解的配置方式,但个人认为纯注解不利于管理,注解太分散,因而只在spring-security的配置上使用一些经过优化的注解配置方式,这方面完全看个人喜好吧.先配一个mvc后端.这里引入了security和utf8两个过滤器,值得一提的是如果不对它们进行配置,则启动顺序为自然顺序,即谁的<filter-mapping>先出现就先启动.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>springmvc-api</display-name>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- 字符编码过滤 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Spring 配置 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:applicationContext.xml</param-value>
    </context-param>

    <!-- Spring MVC 配置 -->
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

在applicationContext.xml中写入相关配置,这部分要注意的是注解扫描 <context:component-scan base-package="com.**"/>一定要指定到com包之下一级,还有各标签的xsd限定网址也要配置正确,否则会报莫名其妙的错.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
">

    <mvc:default-servlet-handler></mvc:default-servlet-handler>
    <context:component-scan base-package="com.**"/>
    <context:property-placeholder location="classpath*:/properties/*.properties"/>
    <import resource="classpath*:/spring/*.xml"/>
    <mvc:annotation-driven></mvc:annotation-driven>

    <bean class="com.share.config.WebSecurityConfig"/>
    <!-- 设置返回json的编码格式 -->
    <bean id="stringConverter" class="org.springframework.http.converter.StringHttpMessageConverter">
        <property name="supportedMediaTypes">
            <list>
                <value>application/json;charset=UTF-8</value>
            </list>
        </property>
    </bean>
    <bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"></bean>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <ref bean="stringConverter" />
                <ref bean="jsonConverter" />
            </list>
        </property>
    </bean>
</beans>

写一个controller,启动后会显示index页面.这里的index.jsp仅作为指示页面,证明服务器正常启动,生产环境中只提供json服务,将其废弃.

@RestController
@RequestMapping("/api")
@ResponseBody
public class UsersStationRest {

    @Autowired
    private UsersStationService usersStationService;

    @RequestMapping("/get/{id}")
    @PreAuthorize("hasAnyAuthority('ADMIN')")
    public UsersStation getUsersFromMysql(@PathVariable Long id) {
        return usersStationService.getUsersStationById(id);
    }
}

欢迎页面

确定controller可以正常启动后,引入mybatis和redis的配置,使用mysql作为持久化数据库,使用redis作为内在级缓存.这里本来想在生成token后将token缓存到redis里,后来发觉没必要.一是这样做并没有提高安全性,高手们拿到token后仍然为所欲为,二是spring-cache提供了简便方案,直接在service层就能解决缓存问题,而不将其耦合到rest-controller层,故采用spring-cache的方案.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <bean id="dataSourceDbcp2" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${user}"/>
        <property name="password" value="${password}"/>

        <property name="initialSize" value="${initialSize}"/>
        <property name="maxIdle" value="${maxIdle}"/>
        <property name="maxTotal" value="${maxTotal}"/>
        <property name="maxWaitMillis" value="${maxWaitMillis}"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSourceDbcp2"/>
        <property name="mapperLocations" value="classpath*:/com.**.mapper/*.xml"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.**.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSourceDbcp2"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">


    <bean id="redisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!-- 最大空连接数 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!-- 最大等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!-- 返回连接时,检测连接是否成功 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    </bean>

    <!-- Spring-redis连接池管理工厂 -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- IP地址 -->
        <property name="hostName" value="${redis.host}" />
        <!-- 端口号 -->
        <property name="port" value="${redis.port}" />
        <property name="password" value="${redis.password}" />
        <!-- 超时时间 默认2000-->
        <property name="timeout" value="${redis.timeout}" />
        <!-- 连接池配置引用 -->
        <property name="poolConfig" ref="redisPoolConfig" />
        <!-- usePool:是否使用连接池 -->
        <property name="usePool" value="true"/>
    </bean>

    <!-- redis template definition -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer" />
        </property>
        <!--开启事务  -->
        <property name="enableTransactionSupport" value="true"></property>
    </bean>

    <!--自定义redis工具类,在需要缓存的地方注入此类  -->
    <bean id="redisService" class="com.share.redis.RedisService">
        <constructor-arg name="redisTemplate" ref="redisTemplate"/>
    </bean>

    <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate"/>
        <property name="defaultExpiration" value="${redis.expiration}"/>
    </bean>

    <cache:annotation-driven cache-manager="redisCacheManager"/>

</beans>

在数据库里建一张表,考虑到解耦合的需要,只在user表里提供用户名/密码/角色等主要信息,后续其它功能采用两表关联的方式解决


用户表

说一下redis.在3.0之后,巨硬放弃了windows下的redis项目,redis表面上变成了只能在linux环境使用的软件,这充分调动起广大程序员的热情.使用cygwin编译即可,具体流程参考这里.当redis5在windows下成功启动,我想起了十七岁的夏天.

redis5

至此万事俱备,正式开始实现基于jwtToken的无状态登录.首先搞一个config配置类

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Configuration
    public static class MySecurityConfig extends WebSecurityConfigurerAdapter {

        @Autowired
        @Qualifier("databaseUserDetailService")
        private DatabaseUserDetailsService databaseUserDetailsService;

        @Autowired
        @Qualifier("authenticationFailHandler")
        private AuthenticationFailHandler failHandler;

        @Autowired
        @Qualifier("authenticationSuccessHandler")
        private AuthenticationSuccessHandler successHandler;

        @Autowired
        @Qualifier("authenticationEntryPointImpl")
        private AuthenticationEntryPointImpl entryPoint;

        @Autowired
        private JwtAuthenticationFilter jwtAuthenticationFilter;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().csrf().disable().authorizeRequests()
                    .antMatchers("/static/**", "/index.jsp", "/api/signup").permitAll()
                    .anyRequest().authenticated()
                    .and().formLogin().loginProcessingUrl("/api/login")
//                    .and().logout().addLogoutHandler()
                    .successHandler(successHandler)
                    .failureHandler(failHandler)
                    .and().exceptionHandling().authenticationEntryPoint(entryPoint);
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(databaseUserDetailsService).passwordEncoder(new Md5PasswordEncoder(){
                @Override
                public String encodePassword(String rawPass, Object salt) {
                    return super.encodePassword(rawPass, "");
                }

                @Override
                public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
                    return super.isPasswordValid(encPass, rawPass, "");
                }
            });
        }

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
}

简单理解的话,这里重载了默认的security源码,除了给定允许直接访问和须经鉴权访问的路径外,将其余操作委托给handler处理.实际上security跟spring一样,有一个context环境,用来存储用户相关的权限信息,相当于扩展了session的作用.这里还要注意给每个handler定义一个标识符,否则可能会与默认的handler冲突.按这个思路,实现各个handler即可.

@Service("authenticationSuccessHandler")
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        System.out.println("User: " + request.getParameter("username") + " Login successfully.");
        this.returnJson(response, authentication);
    }

    private void returnJson(HttpServletResponse response, Authentication authentication) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding(CommonEnum.UTF_8.getName());
        response.setContentType(CommonEnum.APPLICATION_JSON.getName());
        response.getWriter()
                .println("{\"tokenType\":\"Bearer\",\"token\": \""
                        + jwtTokenProvider.createJwtToken(authentication) + "\"}");

    }
}

@Service("authenticationFailHandler")
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        this.returnJson(response, exception);
    }

    private void returnJson(HttpServletResponse response, AuthenticationException exception) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding(CommonEnum.UTF_8.getName());
        response.setContentType(CommonEnum.APPLICATION_JSON.getName());
        response.getWriter().println("{\"exceptionId\":\"null\",\"messageCode\":\"401\"," +
                "\"message\": \""+ exception.getMessage() +"\",\"serverTime\": " + System.currentTimeMillis() +"}");
    }
}

@Service("authenticationEntryPointImpl")
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

至此,一个鉴权流程已经完整实现出来了,验证一下.


登录操作

可以看到已经可以成功返回token了,此时将token置于Authorization下,访问api/get/1这个只有ADMIN才能访问的rest地址,成功.


登录获取token 携带token获取资源 用角色为"COMMON"的用户访问资源,惨遭禁止

补充有关@Cacheable的配置,实际上使用默认的方式就可以

    @Transactional
    @Cacheable(value = "users", key = "#username")
    public List<UsersStation> getUsersStationByUsername(String username) {
        List<UsersStation> users = new ArrayList<>();
        try {
            users = usersStationMapper.getUsersStationByUsername(username);
            return users;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

这里还有最后一个坑,那就是cache缓存的只能是从mysql里查询出来的第一个值,它不承认整个方法返回的变量,也就是说它认为只要经过处理,数据就"脏"了,所以一般把cache加到原子操作的service或dao层.spring-security在shiro的基础上优化了鉴权性能,提供了更多的微粒化操作,是未来的应用方向.
参考文章,这里给出了详细思路,可供参考.

相关文章

网友评论

      本文标题:搭建Json服务器并实现鉴权

      本文链接:https://www.haomeiwen.com/subject/myoepctx.html