SpringSecurity+OAuth2.0怎么玩?
共 108850字,需浏览 218分钟
·
2021-04-10 10:49
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | 梦游的龙猫
来源 | urlify.cn/VzeQ7b
前言
关于 OAuth2.0的认证体系,翻阅了好多资料,RCF 文档太多,看了一半就看不下去了,毕竟全英文的文档看起来,是有一点让我烦躁,但也对 OAuth2.0的认证流程有了一个基本的概念,之前用 SpringSecurity 做了一个基于 RBAC 的权限管理系统的基础配置,所以对 SpringSecurity 算是比较了解了,于是 OAuth2.0的实现,也想用 SpringSecurity 的来做,心想应该比较简单,然而...事实上,我反反复复,拿起又放弃,放弃又拿起,来来回回折腾了3个多月,才真正的掌握了这个 OAuth2.0插件(OAuth2.0不是一个独立的框架,只是 SpringSecurity 的一个插件而已)。
官网的 Demo 配置,是基于 JavaConfig 的配置方式,以前都用 XML 的,没接触过 JavaConfig,所以又绕了一圈,把 JavaConfig 方式的所有框架(Spring、SpringMVC、Mybatis、SpringSecurity、Web.xml)基本配置方式都走了一圈, 确实,全代码配置是很酷,很清爽,说实话,今后我也会逐渐往这方面走,因为这个方式比较有代码感,哈哈,但是现在还不行,因为有很多插件啊、特殊的配置方式啊,我都还不清楚要怎么配置,处于安全考虑,还是老老实实的用 XML 的比较好。
额外插播一则我团队的招聘广告:
阿里巴巴 - 淘系技术部招聘:https://www.cnblogs.com/wuxinzhe/p/11258226.html
项目的说明
网上有很多,SpringSecurityOAuth2.0的配置文章,但是每个文章,都是将认证服务器和资源服务器写在一起的,并没有将认证与资源分离,也没有讲不同的资源之间如何拆分,然而我们在设计分布式系统的时候,总会以模块化的方式,将不同的资源写成不同的项目,比如,将网站的一个电商系统,专门写成一个项目,把网站中的论坛系统,写成另一个项目,部署的时候,每个项目就可以单独部署,后端系统均以 RESTFull 的方式开放数据接口(RESTFull就是推荐使用 OAuth2.0的方式进行认证管理)。这样的方式来设计程序,最大的优点就是模块之间相互独立,互不干涉,在开发工作当中,可以并行开发,单独维护,同时模块分离出来,今后还可以进行很便利的集群,而不需要修改任何原来的代码,所以对整个项目的扩展性是非常好的,不同的项目之间,可以简单的使用 HttpClient 进行通讯,OAuth2.0五种授权模式当中,有一种授权模式就是为这种资源服务器之间的通讯而设计的。
认证服务器与资源服务器分离的这个配置方式,同时也实现了“统一认证”的模式,只需要在认真服务器上做了认证,拿到了 Token,就可以访问所有授权的资源服务器。
接下来,我们开始搭建认证服务器的配置。
POM
项目用到的框架有这几个:Spring、SpringSecurity、Mybatis
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6 <groupId>Showings</groupId>
7 <artifactId>OAuthServer</artifactId>
8 <version>1.0-SNAPSHOT</version>
9 <build>
10 <finalName>showings</finalName>
11 <plugins>
12 <!--Mybatis 逆向工程插件-->
13 <plugin>
14 <groupId>org.mybatis.generator</groupId>
15 <artifactId>mybatis-generator-maven-plugin</artifactId>
16 <version>1.3.2</version>
17 <configuration>
18 <verbose>true</verbose>
19 <overwrite>true</overwrite>
20 </configuration>
21 </plugin>
22 <plugin>
23 <groupId>org.apache.maven.plugins</groupId>
24 <artifactId>maven-compiler-plugin</artifactId>
25 <configuration>
26 <source>1.7</source>
27 <target>1.7</target>
28 </configuration>
29 </plugin>
30 </plugins>
31 </build>
32 <properties>
33 <security.version>4.2.2.RELEASE</security.version>
34 <spring.version>4.3.7.RELEASE</spring.version>
35 <security.oauth.version>2.0.7.RELEASE</security.oauth.version>
36 </properties>
37 <dependencies>
38 <!-- SpringFramework Start -->
39 <dependency>
40 <groupId>org.springframework</groupId>
41 <artifactId>spring-core</artifactId>
42 <version>${spring.version}</version>
43 </dependency>
44
45 <dependency>
46 <groupId>org.springframework</groupId>
47 <artifactId>spring-web</artifactId>
48 <version>${spring.version}</version>
49 </dependency>
50
51 <dependency>
52 <groupId>org.springframework</groupId>
53 <artifactId>spring-oxm</artifactId>
54 <version>${spring.version}</version>
55 </dependency>
56
57 <dependency>
58 <groupId>org.springframework</groupId>
59 <artifactId>spring-tx</artifactId>
60 <version>${spring.version}</version>
61 </dependency>
62
63 <dependency>
64 <groupId>org.springframework</groupId>
65 <artifactId>spring-webmvc</artifactId>
66 <version>${spring.version}</version>
67 </dependency>
68
69 <dependency>
70 <groupId>org.springframework</groupId>
71 <artifactId>spring-aop</artifactId>
72 <version>${spring.version}</version>
73 </dependency>
74
75 <dependency>
76 <groupId>org.springframework</groupId>
77 <artifactId>spring-context-support</artifactId>
78 <version>${spring.version}</version>
79 <!--排除自带的日志工具,从而转向使用SLF4J日志-->
80 <exclusions>
81 <exclusion>
82 <groupId>commons-logging</groupId>
83 <artifactId>commons-logging</artifactId>
84 </exclusion>
85 </exclusions>
86 </dependency>
87
88 <dependency>
89 <groupId>org.springframework</groupId>
90 <artifactId>spring-expression</artifactId>
91 <version>${spring.version}</version>
92 </dependency>
93 <!-- SpringFramework End -->
94 <dependency>
95 <groupId>javax.validation</groupId>
96 <artifactId>validation-api</artifactId>
97 <version>2.0.0.Alpha2</version>
98 </dependency>
99 <!--数据有效性验证框架-->
100 <dependency>
101 <groupId>org.hibernate</groupId>
102 <artifactId>hibernate-validator</artifactId>
103 <version>6.0.0.Alpha2</version>
104 </dependency>
105 <!--c3p0-->
106 <dependency>
107 <groupId>com.mchange</groupId>
108 <artifactId>c3p0</artifactId>
109 <version>0.9.5.1</version>
110 </dependency>
111 <!--Mybatis-->
112 <dependency>
113 <groupId>org.mybatis</groupId>
114 <artifactId>mybatis</artifactId>
115 <version>3.3.0</version>
116 </dependency>
117 <!--Mybatis分页工具 pageHelper-->
118 <dependency>
119 <groupId>com.github.pagehelper</groupId>
120 <artifactId>pagehelper</artifactId>
121 <version>4.1.6</version>
122 </dependency>
123 <!--分页搭配SQL解析工具-->
124 <dependency>
125 <groupId>com.github.jsqlparser</groupId>
126 <artifactId>jsqlparser</artifactId>
127 <version>0.9.6</version>
128 </dependency>
129 <!--Mybatis Spring整合-->
130 <dependency>
131 <groupId>org.mybatis</groupId>
132 <artifactId>mybatis-spring</artifactId>
133 <version>1.2.3</version>
134 </dependency>
135
136 <!--MySQL Driver-->
137 <dependency>
138 <groupId>mysql</groupId>
139 <artifactId>mysql-connector-java</artifactId>
140 <version>5.1.6</version>
141 </dependency>
142 <dependency>
143 <groupId>jstl</groupId>
144 <artifactId>jstl</artifactId>
145 <version>1.2</version>
146 </dependency>
147 <!-- https://mvnrepository.com/artifact/javax.el/javax.el-api -->
148 <dependency>
149 <groupId>javax.el</groupId>
150 <artifactId>javax.el-api</artifactId>
151 <version>3.0.1-b04</version>
152 </dependency>
153
154 <!--spring security-->
155 <dependency>
156 <groupId>org.springframework.security</groupId>
157 <artifactId>spring-security-core</artifactId>
158 <version>${security.version}</version>
159 </dependency>
160 <dependency>
161 <groupId>org.springframework.security</groupId>
162 <artifactId>spring-security-web</artifactId>
163 <version>${security.version}</version>
164 </dependency>
165 <dependency>
166 <groupId>org.springframework.security</groupId>
167 <artifactId>spring-security-taglibs</artifactId>
168 <version>${security.version}</version>
169 </dependency>
170 <dependency>
171 <groupId>org.springframework.security</groupId>
172 <artifactId>spring-security-config</artifactId>
173 <version>${security.version}</version>
174 </dependency>
175
176 <dependency>
177 <groupId>org.springframework.security.oauth</groupId>
178 <artifactId>spring-security-oauth2</artifactId>
179 <version>${security.oauth.version}</version>
180 </dependency>
181
182 <!--SLF4J日志 start-->
183 <dependency>
184 <groupId>org.slf4j</groupId>
185 <artifactId>slf4j-api</artifactId>
186 <version>1.7.10</version>
187 </dependency>
188 <dependency>
189 <groupId>ch.qos.logback</groupId>
190 <artifactId>logback-classic</artifactId>
191 <version>1.1.2</version>
192 </dependency>
193 <dependency>
194 <groupId>ch.qos.logback</groupId>
195 <artifactId>logback-core</artifactId>
196 <version>1.1.2</version>
197 </dependency>
198 <!--SLF4J日志 end-->
199
200 <dependency>
201 <groupId>javax.servlet</groupId>
202 <artifactId>javax.servlet-api</artifactId>
203 <version>3.1.0</version>
204 </dependency>
205
206 <!--Jackson start-->
207 <dependency>
208 <groupId>org.codehaus.jackson</groupId>
209 <artifactId>jackson-mapper-asl</artifactId>
210 <version>1.9.13</version>
211 </dependency>
212 <dependency>
213 <groupId>com.fasterxml.jackson.core</groupId>
214 <artifactId>jackson-annotations</artifactId>
215 <version>2.6.1</version>
216 </dependency>
217 <dependency>
218 <groupId>com.fasterxml.jackson.core</groupId>
219 <artifactId>jackson-core</artifactId>
220 <version>2.6.1</version>
221 </dependency>
222 <dependency>
223 <groupId>com.fasterxml.jackson.core</groupId>
224 <artifactId>jackson-databind</artifactId>
225 <version>2.6.1</version>
226 </dependency>
227 <!--Jackson end-->
228
229 </dependencies>
230
231 </project>
Pom
Pom 很长,但其实没有多少内容,我们需要自己写的代码,也非常非常非常的少...= =,是不是很开心?嘿嘿...
项目目录结构
Yes,你没看错,目录内容真的很少...Java 的部分,真的就没几个= =
配置文件
首先是 Dao 的配置文件:
application-dao.xml
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--获取数据库配置文件-->
<context:property-placeholder location="classpath:config/db.properties"/>
<context:component-scan base-package="cn.com.showings.mapper"/>
<!--设置数据源c3p0-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="maxPoolSize" value="50"/>
<property name="minPoolSize" value="2"/>
<property name="maxIdleTime" value="60"/>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:config/mybatis-config.xml"/>
<property name="dataSource" ref="dataSource"/>
<!-- 显式指定Mapper文件位置 -->
<property name="mapperLocations">
<list>
<value>classpath*:/mapper/*.xml</value>
</list>
</property>
</bean>
<!--自动扫描mapper接口,并注入sqlsession-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.com.showings.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSession"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>这边可以看到,我们明明使用的是 Mybatis 的 ORM框架,为啥还要配置一个 jdbcTemplate?其实说来惭愧,本人比较擅长 MyBatis,所以算是强迫症一定要用这个框架,但是人家 Spring 有自己的 SpringData 的框架,而 SpringSecurityOAuth2.0的插件中,很多内容都是用 SpringData 的方式去实现的,我如果要弃用 jdbcTemplate,那我得重写所有框架内涉及的数据库操作,那太累了- -,当然啦,我这么写肯定不好,因为认证系统本身没有什么复杂逻辑和除了框架外的额外操作,所以我这么做,挺浪费资源的(占内存),大家可以不要效仿这一块,用到 MyBatis 的地方只有一个读取用户名及密码的接口,也就是说,为了一个接口,确实没有必要引入一个框架。等我掌握了 jdbcTemplate 的用法,我也会去掉这个累赘。
当然~!如果,你的认证系统,跟用户管理系统,是合在一起的情况下,那倒是没啥问题,毕竟用户管理也是有很多逻辑的,像注册呀、改密啊、绑定密保啊、修改用户信息呀,这些什么鬼的。
接着我们来配置 Service:
application-service.xml
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--扫描service-->
<context:component-scan base-package="cn.com.showings.service"/>
<!--注册统一异常控制-->
<bean id="exception" class="cn.com.showings.controller.ExceptionController"/>
</beans>没什么内容,看注释就知道了。
然后配置 Transaction:
application-transaction.xml
<?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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--事务管理对象-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--注解事务-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>嗯...事实上,可以不用配置,因为根本用不上。
配置 Spring-mvc:
spring-mvc.xml
<?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 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--自动扫描控制器-->
<context:component-scan base-package="cn.com.showings.controller"/>
<!--控制器映射器和控制器适配器-->
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/fonts/**" location="/fonts/"/>
<!--视图渲染-->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- rest json related... start -->
<bean id="mappingJacksonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="mappingJacksonHttpMessageConverter"/>
</list>
</property>
</bean>
<!-- rest json related... end -->
</beans>这..也没啥可说的,全世界都这么配置的= =
再来配置一个 Mybatis 的分页插件...其实可以不用配置,因为根本用不到,除非以后有啥扩展的话:
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<!-- 4.0.0以后版本可以不设置该参数 -->
<property name="dialect" value="mysql"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="false"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="none"/>
</plugin>
</plugins>
</configuration>这个比较详细,因为这个比较麻烦。所以内容都写的很多,如果你不配置这个,自然对目前来说,也是可以的。
再来一个 LogBack 的配置文件,这个配置文件必须放在配置文件的根目录下,我是使用 IDEA ,maven 的方式搭建项目的,这种配置资源全部都放在 resources 文件夹下,而且文件名字还就得叫这个:
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 定义日志的根目录 -->
<property name="LOG_HOME" value="/Users/wuxinzhe/IdeaProjects/OAuthServer/logs"/>
<!-- 定义日志文件名称 -->
<property name="appName" value="OAuthServer"/>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<Encoding>UTF-8</Encoding>
<!--
日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%n[%d{yyyy-MM-dd HH:mm:ss}] [userID:%X{userID}] [%logger{50}] %n[%-5level] %msg %n
</pattern>
</layout>
</appender>
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Encoding>UTF-8</Encoding>
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${appName}.log</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>
<!--
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!--
日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
%n[%d{yyyy-MM-dd HH:mm:ss}] [userID:%X{userID}] [%logger{50}] %n[%-5level] %msg %n
</pattern>
</layout>
</appender>
<!--
logger主要用于存放日志对象,也可以定义日志类型、级别
name:表示匹配的logger类型前缀,也就是包的前半部分
level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,false:表示只用当前logger的appender-ref,true:表示当前logger的appender-ref和rootLogger的appender-ref都有效
-->
<!--
root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="info">
<appender-ref ref="stdout"/>
<appender-ref ref="appLogAppender"/>
</root>
</configuration>这个东西我想是必须的吧,毕竟,日志应该还是有用的...虽然我整个项目中,都没有输出日志的代码,不过框架还是有日志输出的需求的。= =
还剩下最后一个配置文件,这个配置文件是 MyBatis 的逆向工程插件的配置文件,如果你有用的话,就弄一个吧
generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<classPathEntry
location="/Users/wuxinzhe/.m2/repository/mysql/mysql-connector-java/5.1.6/mysql-connector-java-5.1.6.jar"/>
<context id="testTables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://showings.com.cn:3306/oauth"
userId="root"
password="199176">
</jdbcConnection>
<!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL 和
NUMERIC 类型解析为java.math.BigDecimal -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- targetProject:生成PO类的位置 -->
<javaModelGenerator targetPackage="cn.com.showings.entity"
targetProject="src/main/java">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- targetProject:mapper映射文件生成的位置 -->
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- targetPackage:mapper接口生成的位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="cn.com.showings.mapper"
targetProject="src/main/java">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!--指定数据库表-->
<table tableName="USER_ROLE"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"
enableSelectByExample="false"
selectByExampleQueryId="false"/>
</context>
</generatorConfiguration>好了,最重要的:
application-security.xml
<?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:sec="http://www.springframework.org/schema/security"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">
<sec:http pattern="/js/**" security="none"/>
<sec:http pattern="/fonts/**" security="none"/>
<sec:http pattern="/css/**" security="none"/>
<!-- /oauth/token 是oauth2登陆验证请求的url用于获取access_token ,默认的生存时间是43200秒,即12小时-->
<sec:http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
entry-point-ref="oauth2AuthenticationEntryPoint" use-expressions="false">
<sec:intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY"/>
<sec:anonymous enabled="false"/>
<sec:http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<sec:custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
<sec:access-denied-handler ref="oauth2AccessDeniedHandler"/>
<sec:csrf disabled="true"/>
</sec:http>
<!--处理访问成功-->
<bean id="oauth2AuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<!--处理访问拒绝-->
<bean id="oauth2AccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
<!--处理认证点-->
<bean id="oauthUserApprovalHandler"
class="org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler"/>
<!--处理访问控制-->
<bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
<bean class="org.springframework.security.access.vote.RoleVoter"/>
<bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
</list>
</constructor-arg>
</bean>
<bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<property name="authenticationManager" ref="oauth2AuthenticationManager"/>
</bean>
<!--可访问客户端参数配置,可转成数据库配置-->
<oauth2:client-details-service id="clientDetailsService">
<oauth2:client client-id="web_client"
authorized-grant-types="password,authorization_code,refresh_token,implicit"
secret="web" scope="read,write"/>
</oauth2:client-details-service>
<!--可访问客户端参数配置,数据库管理方案-->
<!--<bean id="clientDetailsService"-->
<!--class="org.springframework.security.oauth2.provider.client.JdbcClientDetailsService">-->
<!--<constructor-arg index="0" ref="dataSource"/>-->
<!--</bean>-->
<bean id="oauth2ClientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<constructor-arg ref="clientDetailsService"/>
</bean>
<sec:authentication-manager id="oauth2AuthenticationManager">
<sec:authentication-provider user-service-ref="oauth2ClientDetailsUserService"/>
</sec:authentication-manager>
<!--Config token services-->
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
<constructor-arg index="0" ref="dataSource"/>
</bean>
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore" ref="tokenStore"/>
<property name="clientDetailsService" ref="clientDetailsService"/>
<property name="supportRefreshToken" value="true"/>
</bean>
<bean id="jdbcAuthorizationCodeServices"
class="org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices">
<constructor-arg index="0" ref="dataSource"/>
</bean>
<!--oauth2 的server所能支持的请求类型-->
<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="oauthUserApprovalHandler"
user-approval-page="oauth_approval"
error-page="oauth_error">
<oauth2:authorization-code authorization-code-services-ref="jdbcAuthorizationCodeServices"/>
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password/>
</oauth2:authorization-server>
<oauth2:resource-server id="webResourceServer" resource-id="web-resource" token-services-ref="tokenServices"/>
<sec:http disable-url-rewriting="true" use-expressions="false" authentication-manager-ref="authenticationManager">
<sec:intercept-url pattern="/oauth/**" access="ROLE_USER"/>
<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:form-login authentication-failure-url="login.html?authorization_error=true"
default-target-url="index.html"
login-page="login.html" login-processing-url="login"/>
<sec:logout logout-success-url="success.html" logout-url="logout"/>
<sec:access-denied-handler error-page="login.html?access_denied=true"/>
<sec:anonymous/>
<sec:csrf disabled="true"/>
</sec:http>
<!-- 验证的权限控制 -->
<sec:authentication-manager id="authenticationManager">
<sec:authentication-provider user-service-ref="userServiceImpl">
<sec:password-encoder hash="md5"/>
</sec:authentication-provider>
</sec:authentication-manager>
</beans>这个文档很长,而且我也必须做一个讲解,否则就算配置了,估计也不知道怎么用。
我们,分段讲解:
1 <sec:http pattern="/js/**" security="none"/>
2 <sec:http pattern="/fonts/**" security="none"/>
3 <sec:http pattern="/css/**" security="none"/>这个,是告诉框架,这三块内容,不需要权限验证,任何人都可以获取,所以对这三个资源,直接绕过 SpringSecurity 框架。
<!-- /oauth/token 是oauth2登陆验证请求的url用于获取access_token ,默认的生存时间是43200秒,即12小时-->
<sec:http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
entry-point-ref="oauth2AuthenticationEntryPoint" use-expressions="false">
<sec:intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY"/>
<sec:anonymous enabled="false"/>
<sec:http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<sec:custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
<sec:access-denied-handler ref="oauth2AccessDeniedHandler"/>
<sec:csrf disabled="true"/>
</sec:http>这部分,是OAuth2.0用于获取 token 的地址,就是上面写的"/oauth/token",上面的配置内容我挑着讲解一下,首先是 “authentication-manager-ref”,这是是认证管理器的指定,意思就是,当访问这个资源的时候,我们要用特定的,专门为获取 token 的认证管理器,这么说可能不理解,那有特定的认证管理器,就肯定有普通的认证管理器,普通的认证管理器,就是之前我配置 RBAC 权限系统的时候,用户的以用户名及密码登录时的那个验证用户密码的管理器,这个是最普通的,也是必须有的管理器,相对这个管理器来说,OAuth2.0需要一个专门对“/oauth/token”这个资源(要获取的 token 本身就是一种资源),有一个认证管理器,认证的是 client-id 与对应的 client-secret,就是对访问客户端的认证,我们对客户端程序是有限制的,不是每个客户端程序都能够访问我们的接口,而是我们资源服务器授权的某个客户端才能有资格来申请,这么说可能还是不太好理解,我用现实中的例子来说明这个问题:
我们都在某些小型的网站,尤其是论坛上看到登录时,可以选择使用 QQ 登录,对吧?这也是 OAuth2.0的一个业务场景,其实这些小型网站要实现这类功能之前,必须先跟腾讯申请合作,然后腾讯会在它自己的客户端数据表中,创建一个属于这个小网站的一个 ID,对于腾讯来说,这个小网站就是一个客户端,他想要获取腾讯的一些资源,比如用户昵称及头像。那小网站 A 跟腾讯签订了合作协议,腾讯同意它作为一个客户端来访问资源(仅仅是说 token 的资源,因为这个针对客户端的授权认证,仅仅只是用来申请 token 的),但是腾讯不允许没有跟他签订合作的其他小网站来访问资源,所以那些没有申请合作的小网站,没有腾讯分发的 ID及密码,自然就不能发起访问申请。而且,OAuth2.0有一个很重要的功能,就是根据不同的客户端,可以区别对待,比如一个比较大一点的中型网站,跟腾讯有付费关系,就是所谓的哈哈,QQ 会员级别,那他跟其他没付费的小网站自然不同,腾讯能让她访问的资源内容的范围肯定也不同,所以通过不同的 client,自然就可以进行区别对待,冠冕堂皇收取 QQ 会员费啦~!哈哈
其他元素,我就不解释了,都有对应的 Bean,对应 Bean 上都有注释,主要是没啥不好理解的,所以就不需要解释了吧。
<!--可访问客户端参数配置,可转成数据库配置-->
<oauth2:client-details-service id="clientDetailsService">
<oauth2:client client-id="web_client"
authorized-grant-types="password,authorization_code,refresh_token,implicit"
secret="web" scope="read,write"/>
</oauth2:client-details-service>这就是刚才我说的,客户端配置,我这边是完全是写死的,因为我的网站没打算建立开放平台给别人,妈蛋,我的网站要有那个能力,大家都来我这里获取资源,我还在这里写代码作死?当然啦,我在底下有注释,用于数据库配置的方式,其实也没啥难的。这边我们可以看一下,authorized-grant-type 是认证类型,五种认证类型,可以根据需要,做一些取舍,一般,常用的是 authorization_code,这个是最常用的手法,资源服务器之间的通讯可以使用implicit,具体这几种方式怎么用,恩,回头我再写一个博客专门讲好了。
<!--oauth2 的server所能支持的请求类型-->
<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="oauthUserApprovalHandler"
user-approval-page="oauth_approval"
error-page="oauth_error">
<oauth2:authorization-code authorization-code-services-ref="jdbcAuthorizationCodeServices"/>
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password/>
</oauth2:authorization-server>这个呢,可以讲得也不多,就是那个 user-approval-page,我讲一下,我们在使用微信的时候,其实微信也是用的 OAuth2.0,只是没有让你输入用户密码,因为你本身就登录着微信,而微信是 Socket 连接,是收信任的链接方式,所以当你点一些小程序或者一些其他基于微信开发的一些程序的时候,就从来不用输入用户密码,但是一定会有一个提示,就是问你是否要授权微信登录,其实就是这样的一个类似的授权页面,如图:
为啥要有这个页面,好像多此一举的感觉呢?很简单咯,以防万一,一些恶意网站,在你不知情的情况下,去调用你的信息。而 error-page 就是当用户点击拒绝访问的时候,跳转的页面。
1 <oauth2:resource-server id="webResourceServer" resource-id="web-resource" token-services-ref="tokenServices"/>
这个是资源 ID,定义资源服务器的,恩...其实我也不知道这个要不要加入到这里,没试过,因为这个是必须要加到资源服务器的配置文件当中的,而认证服务器要不要我还不清楚,因为我目前还没有把资源服务器的内容写起来,反正暂且先写着,如果不需要,回头再去掉就行。
<sec:http disable-url-rewriting="true" use-expressions="false" authentication-manager-ref="authenticationManager">
<sec:intercept-url pattern="/oauth/**" access="ROLE_USER"/>
<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:form-login authentication-failure-url="login.html?authorization_error=true"
default-target-url="index.html"
login-page="login.html" login-processing-url="login"/>
<sec:logout logout-success-url="success.html" logout-url="logout"/>
<sec:access-denied-handler error-page="login.html?access_denied=true"/>
<sec:anonymous/>
<sec:csrf disabled="true"/>
</sec:http>
<!-- 验证的权限控制 -->
<sec:authentication-manager id="authenticationManager">
<sec:authentication-provider user-service-ref="userServiceImpl">
<sec:password-encoder hash="md5"/>
</sec:authentication-provider>
</sec:authentication-manager>这个,就是用户认证系统,不管用RBAC权限管理设计,还是 OAuth2.0协议,都不可避免要输入用户密码,这部分呢就是用来定义这个的。这里有一个坑啊,就是这两个东西,必须放在最后面,因为,我们的拦截地址是拦截/**及/oauth/**,如果写在刚才申请 token 的那个/oauth/token 的配置前面,则会被覆盖,那不管怎么输入用户密码,都不会走用户验证管理器,而是都去走刚才上面说到的那个特殊的,验证client的用户验证管理器。那就不管怎样,都登录不了了= =。
好了,最后就是 Web.xml的配置了。
web.xml
<?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_3_1.xsd"
version="3.1">
<!--设置spring 配置文件的位置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/application-*.xml</param-value>
</context-param>
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>web.root</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.util.WebAppRootListener</listener-class>
</listener>
<!--配置spring listener-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--解决POST乱码问题-->
<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>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<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>
<!--springMVC前端控制器配置-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>login.html</welcome-file>
</welcome-file-list>
</web-app>这个配置很普通,没有什么复杂的。恩,关键的地方说一下,就是 welcome-file-list 这边,我设置成了 login.html,我是这么考虑的:这个认证系统吧,如果真的用起来,可能会比较频繁,所以不希望总是要经过 spirng 及 spring-mvc 来控制,就做成静态页面好了,这样可以提高些效率。而且本身登录也没有什么复杂逻辑,用不到 JSP,所以我把登录页面做成了静态页面,而且希望项目的默认访问地址,就是登录页面,因为本身这就只是一个认证系统而已,没有其他的功能。
但是看一下前面的 Security 配置中,对用户登录认证的部分的配置,我配置了登录失败的一些错误页面,
<sec:form-login authentication-failure-url="login.html?authorization_error=true"
default-target-url="index.html"
login-page="login.html" login-processing-url="login"/>
<sec:access-denied-handler error-page="login.html?access_denied=true"/>首先是“login.html?authorization_error=true”,这个是当认证失败,就是验证用户密码错误的时候,返回的页面,当然还是要回到登录页面,但是带了URL参数,还有一个就是“login.html?access_denied=true”,这个是访问拒绝的页面,就是当你没有登录就想要访问一些资源的时候,会跳转到这个页面,其实还是登录页面,只是携带参数不同,不过...本身认证系统就没有什么其他接口,所以这个可能也没啥用就是了...那反正定义一个,也好。
在 login.html 页面呢,因为是静态的,不能用 jstl 表达式来获取 URL 参数从而显示不同的提示语,但是可以使用 js 来做这个事儿,回头我再贴出 login.html 的代码。
几个页面
login.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>登录验证</title>
</head>
<link rel="stylesheet" href="css/bootstrap.min.css">
<script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<body>
<div class="container">
<div class="row" style="height: 150px"></div>
<div class="row">
<div class="col-xs-4"></div>
<div class="col-xs-4">
<div class="page-header">
<h1 class="text-center">系统登录</h1>
</div>
<form action="/login" method="post">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">
<span class="glyphicon glyphicon-user"></span>
</span>
<input type="text" class="form-control" placeholder="用户名" name="username"
aria-describedby="basic-addon1">
</div>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
<input type="password" class="form-control" placeholder="密码" name="password"
aria-describedby="basic-addon1">
</div>
</div>
<button type="submit" class="btn btn-default pull-right">登录</button>
</form>
</div>
<div class="col-xs-4"></div>
</div>
<div class="row" style="height: 10px">
</div>
<div class="row">
<div class="col-xs-4"></div>
<div class="col-xs-4">
<div id="tip" class="alert alert-danger alert-dismissible hidden" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<strong>提示!</strong>
<span id="tip-message"></span>
</div>
</div>
<div class="col-xs-4"></div>
</div>
</div>
</body>
<script type="text/javascript">
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r !== null) {
return unescape(r[2]);
}
return null;
}
if (getQueryString('authorization_error') !== null) {
$('#tip-message').text('用户密码不正确!');
$('#tip').removeClass('hidden');
} else if (getQueryString('access_denied') !== null) {
$('#tip-message').text('您还尚未登录!');
$('#tip').removeClass('hidden');
} else {
$('#tip').addClass('hidden');
}
</script>
</html>oauth_approval.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE HTML>
<html>
<head>
<title>Oauth Approval</title>
<link rel="stylesheet" href="../css/bootstrap.min.css">
<script type="text/javascript" src="../js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12">
<div class="page-header">
<h1 class="text-center">你是否授权"${authorizationRequest.clientId}"访问你的个人信息?</h1>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4"></div>
<div class="col-xs-4">
<form id="oauth-form" action="${pageContext.request.contextPath}/oauth/authorize" method="post">
<input id="approval" name='user_oauth_approval' type='hidden'/>
<div class="btn-group btn-group-justified">
<div class="btn-group" role="group">
<button id="access_authorize" class="btn btn-lg btn-primary" type="button">同意授权</button>
</div>
<div class="btn-group" role="group">
<button id="access_denied" class="btn btn-lg btn-danger" type="button">拒绝访问</button>
</div>
</div>
</form>
</div>
<div class="col-xs-4"></div>
</div>
</div>
<script type="text/javascript">
var approval = $('#approval');
$('#access_authorize').on('click', function () {
approval.val('true');
$('#oauth-form').submit();
});
$('#access_denied').on('click', function () {
approval.val('false');
$('#oauth-form').submit();
});
</script>
</body>
</html>oauth_error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE HTML>
<html>
<head>
<title>授权失败</title>
</head>
<body>
<h3>
授权失败!
</h3>
<div class="alert alert-danger">
<c:out value="${error.summary}"/>
</div>
</body>
</html>
几个类
ExceptionController.java
这个是统一异常处理,这边针对非 Ajax 请求,返回 error 页面,但是我没有写 Error 页面,懒得写啦,因为根本用不到,但是该补还是得补上,以后再补好了。这个我就说明一下,免得有朋友看不懂这块。
package cn.com.showings.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 知识产权声明:本文件自创建起,其内容的知识产权即归属于原作者,任何他人不可擅自复制或模仿.
* 创建者: wu 创建时间: 16/9/29
* 类说明: 统一异常处理
*/
public class ExceptionController implements HandlerExceptionResolver {
private static Logger logger = LoggerFactory.getLogger(ExceptionController.class);
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) {
logger.error(ex.getMessage(), ex);
ModelAndView modelAndView = new ModelAndView();
if (isAjaxRequest(httpServletRequest)) {
modelAndView.setView(new MappingJackson2JsonView());
} else {
modelAndView.setViewName("error");
}
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
modelAndView.addObject("error_info", ex.getMessage());
return modelAndView;
}
private boolean isAjaxRequest(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")) || !StringUtils.isEmpty(request.getParameter("jsonp"));
}
}UserServiceImpl.java
这个类是实现了 UserService 接口,而 UserService 接口内没有任何内容,只是为了留给以后万一要集成用户管理系统或者其他用户操作,故意加的一层接口。UserService 接口继承了 SpringSecurity 框架中的 UserDetailsService,所以这个类目前只实现了一个方法,就是唯一需要使用 mybatis 读取数据库的接口= =。。如果后期不打算扩展其他功能,单纯就只用来认证,那可去掉 Mybatis,直接jdbcTemplate 来查询用户信息。
package cn.com.showings.service.impl;
import cn.com.showings.entity.MyUserDetail;
import cn.com.showings.entity.User;
import cn.com.showings.mapper.UserMapper;
import cn.com.showings.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 类说明:用户服务,管理用户注册、读取用户信息等用户相关操作
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Not found any user for username[" + username + "]");
} else {
return new MyUserDetail(user);
}
}
}MyUserDetail.java
这个类,是用来实现框架中的 UserDetails 接口的,框架中没有什么实体类这种概念,都是用接口实现的,我们这边就主要按照标准,组装一下这个对象的数据结构。
package cn.com.showings.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 类说明:实现 UserDetail
*/
public class MyUserDetail implements UserDetails {
private static final long serialVersionUID = 3006176344390176165L;
private User user;
private static final String ROLE_PREFIX = "ROLE_";
private List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
public MyUserDetail(User user) {
this.user = user;
initAuthority();
}
private void initAuthority() {
List<Role> roleList = user.getRoleList();
for (Role role : roleList) {
this.grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));
}
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorities;
}
public String getPassword() {
return this.user.getPassword();
}
public String getUsername() {
return this.user.getUsername();
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
public User getUser() {
return user;
}
}剩下就是 User 类和 Role 类,以及根据数据库多对多映射出来的 UserRole 类,都是实体类,没啥好说的,无非就是根据数据库映射出来的几个字段而已。
OK 啦,认证服务器的配置就配置完了。资源服务器呢,其实没有啥特别的,除了 SpringSecurity 的那个配置文件,其他都跟授权服务器一样,当然资源服务器要扩展一些自己的功能,肯定还有一些特殊的东西,那反正就权限认证这块,我贴出代码,其他的配置,你们都可以按照授权服务器来配置,配置一些比如 dao,spirng,mvc,logback,mybatis,所以,上面那些其实也不是没用的啦= =
好了,区别就在于 security 的配置:
<?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:sec="http://www.springframework.org/schema/security"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.2.xsd
http://www.springframework.org/schema/security/oauth2
http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">
<sec:http pattern="/**" create-session="never" entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager" use-expressions="false">
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="ROLE_USER,SCOPE_READ"/>
<sec:custom-filter ref="webResourceServer" before="PRE_AUTH_FILTER"/>
<sec:access-denied-handler ref="oauth2AccessDeniedHandler"/>
<sec:csrf disabled="true"/>
</sec:http>
<!--处理访问成功-->
<bean id="oauth2AuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<!--处理访问拒绝-->
<bean id="oauth2AccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
<!--处理认证点-->
<bean id="oauthUserApprovalHandler"
class="org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler"/>
<!--处理访问控制-->
<bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
<bean class="org.springframework.security.access.vote.RoleVoter"/>
<bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
</list>
</constructor-arg>
</bean>
<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
<constructor-arg index="0" ref="dataSource"/>
</bean>
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore" ref="tokenStore"/>
<property name="supportRefreshToken" value="true"/>
</bean>
<oauth2:resource-server id="webResourceServer" resource-id="web-resource" token-services-ref="tokenServices"/>
</beans>
这边的 DataSource要说明一下,这个连接的数据库跟认证服务器的数据库要是一样的,所以这个项目如果数据库分库的话,自然要有多个 dataSource,建议也是分开,认证系统不要跟业务系统混在一起,从根本上区分开每个模块,数据库也要分开。而这个 dataSource 因为链接的跟认证系统是相同的数据库,所以自然就是一个统一认证的模式,只要在认证系统内认证过的,获取的 token 在其他模块中也会到相同的数据库中去验证。另外说一下,这个资源服务器的配置内容,有一点乱,可能还可以继续精简一些,资源服务器我还没有深入去看,可能这也已经是最小配置了。
对了,数据库不能忘了。
/*
Navicat Premium Data Transfer
Source Server : Showings
Source Server Type : MySQL
Source Server Version : 50554
Source Host : 120.25.99.8
Source Database : oauth
Target Server Type : MySQL
Target Server Version : 50554
File Encoding : utf-8
Date: 04/21/2017 09:08:54 AM
*/
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for `oauth_access_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(255) DEFAULT NULL,
KEY `token_id_index` (`token_id`),
KEY `authentication_id_index` (`authentication_id`),
KEY `user_name_index` (`user_name`),
KEY `client_id_index` (`client_id`),
KEY `refresh_token_index` (`refresh_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `oauth_client_details`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` text,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`archived` tinyint(1) DEFAULT '0',
`trusted` tinyint(1) DEFAULT '0',
`autoapprove` varchar(255) DEFAULT 'false',
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `oauth_code`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) DEFAULT NULL,
`authentication` blob,
KEY `code_index` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `oauth_refresh_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication` blob,
KEY `token_id_index` (`token_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `ROLE`
-- ----------------------------
DROP TABLE IF EXISTS `ROLE`;
CREATE TABLE `ROLE` (
`ID` int(2) NOT NULL AUTO_INCREMENT,
`NAME` varchar(10) NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `USER`
-- ----------------------------
DROP TABLE IF EXISTS `USER`;
CREATE TABLE `USER` (
`UID` varchar(255) NOT NULL,
`CREATE_TIME` datetime DEFAULT NULL,
`PASSWORD` varchar(255) NOT NULL,
`USERNAME` varchar(255) NOT NULL,
`LAST_LOGIN_TIME` datetime DEFAULT NULL,
PRIMARY KEY (`UID`),
UNIQUE KEY `guid` (`UID`),
UNIQUE KEY `username` (`USERNAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `USER_ROLE`
-- ----------------------------
DROP TABLE IF EXISTS `USER_ROLE`;
CREATE TABLE `USER_ROLE` (
`ID` int(255) NOT NULL AUTO_INCREMENT,
`USER_UID` varchar(256) NOT NULL,
`ROLE_ID` int(1) NOT NULL,
PRIMARY KEY (`ID`),
KEY `user_id_index` (`USER_UID`(255))
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
这个。补充说一下,用户密码要经过 MD5加密,验证的时候,也是有加密,所以存入数据库的时候,要是没有加密就会验证不通过。
锋哥最新SpringCloud分布式电商秒杀课程发布
👇👇👇
👆长按上方微信二维码 2 秒
感谢点赞支持下哈