基于springboot的web项目最佳实践
- web
- 单元测试
- actuator应用监控
- lombok
- baseEntity
- 统一响应返回值
- 异常
- 数据校验
- 日志
- swagger
- 数据库连接池
- spring jdbc
- jpa
- redis
- spring cache
- mogodb
- mybatis
- spring security
- 项目上下文
- 单点登录
- 邮件
- maven
- 总结
springboot
可以说是现在做javaweb
开发最火的技术,我在基于springboot
搭建项目的过程中,踩过不少坑,发现整合框架时并非仅仅引入starter
那么简单。
要做到简单,易用,扩展性更好,还需做不少二次封装,于是便写了个基于springboot
的web项目脚手架,对一些常用的框架进行整合,并进行了简单的二次封装。
项目名baymax
取自动画片超能陆战队里面的大白,大白是一个医护充气机器人,希望这个项目你能像大白一样贴心,可以减少你的工作量。
github https://github.com/zhaoguhong/baymax
web
web模块是开发web项目必不可少的一个模块
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
对于前后端分离项目,推荐直接使用@RestController
注解
需要注意的是,不建议直接用RequstMapping注解并且不指定方法类型的写法,推荐使用GetMaping
或者PostMaping
之类的注解
@SpringBootApplication
@RestController
public class BaymaxApplication {
public static void main(String[] args) {
SpringApplication.run(BaymaxApplication.class, args);
}
@GetMapping("/test")
public String test() {
return "hello baymax";
}
}
单元测试
spring 对单元测试也提供了很好的支持
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
添加 @RunWith(SpringRunner.class)
和 @SpringBootTest
即可进行测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
}
对于Controller
层的接口,可以直接用MockMvc
进行测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void testValidation() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.content().string("hello baymax"));
}
}
actuator应用监控
actuator 是 spring 提供的应用监控功能,常用的配置项如下
# actuator端口 默认应用端口
management.server.port=8082
# 加载所有的端点 默认只加载 info,health
management.endpoints.web.exposure.include=*
# actuator路径前缀,默认 /actuator
management.endpoints.web.base-path=/actuator
lombok
lombok可以在编译期生成对应的java代码,使代码看起来更简洁,同时减少开发工作量
用lombok后的实体类
@Data
public class Demo {
private Long id;
private String userName;
private Integer age;
}
需要注意,@Data
包含 @ToString、@Getter、@Setter、@EqualsAndHashCode、@RequiredArgsConstructor
,RequiredArgsConstructor 并不是无参构造,无参构造的注解是NoArgsConstructor
RequiredArgsConstructor
会生成 会生成一个包含常量(final),和标识了@NotNull的变量 的构造方法
baseEntity
把表中的基础字段抽离出来一个BaseEntity,所有的实体类都继承该类
/**
* 实体类基础类
*/
@Data
public abstract class BaseEntity implements Serializable {
/**
* 主键id
*/
private Long id;
/**
* 创建人
*/
private Long createdBy;
/**
* 创建时间
*/
private Date createdTime;
/**
* 更新人
*/
private Long updatedBy;
/**
* 更新时间
*/
private Date updatedTime;
/**
* 是否删除
*/
private Integer isDeleted;
}
统一响应返回值
前后端分离项目基本上都是ajax调用,所以封装一个统一的返回对象有利于前端统一处理
/**
* 用于 ajax 请求的响应工具类
*/
@Data
public class ResponseResult<T> {
// 未登录
public static final String UN_LOGIN_CODE = "401";
// 操作失败
public static final String ERROR_CODE = "400";
// 服务器内部执行错误
public static final String UNKNOWN_ERROR_CODE = "500";
// 操作成功
public static final String SUCCESS_CODE = "200";
// 响应信息
private String msg;
// 响应code
private String code;
// 操作成功,响应数据
private T data;
public ResponseResult(String code, String msg, T data) {
this.msg = msg;
this.code = code;
this.data = data;
}
}
返回给前端的值用ResponseResult
包装一下
/**
* 测试成功的 ResponseResult
*/
@GetMapping("/successResult")
public ResponseResult<List<Demo>> test() {
List<Demo> demos = demoMapper.getDemos();
return ResponseResult.success(demos);
}
/**
* 测试失败的 ResponseResult
*/
@GetMapping("/errorResult")
public ResponseResult<List<Demo>> demo() {
return ResponseResult.error("操作失败");
}
ResponseEntity
spring其实封装了ResponseEntity 处理响应,ResponseEntity 包含 状态码,头部信息,响应体 三部分
/**
* 测试请求成功
* @return
*/
@GetMapping("/responseEntity")
public ResponseEntity<String> responseEntity() {
return ResponseEntity.ok("请求成功");
}
/**
* 测试服务器内部错误
* @return
*/
@GetMapping("/InternalServerError")
public ResponseEntity<String> responseEntityerror() {
return new ResponseEntity<>("出错了", HttpStatus.INTERNAL_SERVER_ERROR);
}
异常
自定义异常体系
为了方便异常处理,定义一套异常体系,BaymaxException 做为所有自定义异常的父类
// 项目所有自定义异常的父类
public class BaymaxException extends RuntimeException
// 业务异常 该异常的信息会返回给用户
public class BusinessException extends BaymaxException
// 用户未登录异常
public class NoneLoginException extends BaymaxException
全局异常处理
对所有的异常处理后再返回给前端
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(value = {BusinessException.class})
public ResponseResult<?> handleBusinessException(BusinessException ex) {
String msg = ex.getMessage();
if (StringUtils.isBlank(msg)) {
msg = "操作失败";
}
return ResponseResult.error(msg);
}
/**
* 处理未登录异常
*/
@ExceptionHandler(value = {NoneLoginException.class})
public ResponseResult<?> handleNoneLoginException(NoneLoginException ex) {
return ResponseResult.unLogin();
}
异常持久化
对于未知的异常,保存到数据库,方便后续排错
需要说明是的,如果项目访问量比较大,推荐用 ELK 这种成熟的日志分析系统,不推荐日志保存到关系型数据库
@Autowired
private ExceptionLogMapper exceptionLogMapper;
/**
* 处理未知的错误
*/
@ExceptionHandler(value = {Exception.class})
public ResponseResult<Long> handleunknownException(Exception ex) {
ExceptionLog log = new ExceptionLog(new Date(), ExceptionUtils.getStackTrace(ex));
exceptionLogMapper.insert(log);
ResponseResult<Long> result = ResponseResult.unknownError("服务器异常:" + log.getId());
result.setData(log.getId());
return result;
}
异常日志接口
对外开一个异常日志查询接口/anon/exception/{异常日志id}
,方便查询
@RestController
@RequestMapping("/anon/exception")
public class ExceptionController {
@Autowired
private ExceptionLogMapper exceptionLogMapper;
@GetMapping(value = "/{id}")
public String getDemo(@PathVariable(value = "id") Long id) {
return exceptionLogMapper.selectByPrimaryKey(id).getException();
}
}
数据校验
JSR 303 定义了一系列的 Bean Validation 规范,Hibernate Validator 是 Bean Validation 的实现,并进行了扩展
spring boot 使用 也非常方便
public class Demo extends BaseEntity{
@NotBlank(message = "用户名不允许为空")
private String userName;
@NotBlank
private String title;
@NotNull
private Integer age;
}
参数前面添加@Valid注解即可
@PostMapping("/add")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}
对于校验结果可以每个方法单独处理,如果不处理,会抛出有异常,可以对校验的异常做全局处理
在 GlobalControllerExceptionHandler
添加
/**
* 处理校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult<?> handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
if (result.hasErrors()) {
StringJoiner joiner = new StringJoiner(",");
List<ObjectError> errors = result.getAllErrors();
errors.forEach(error -> {
FieldError fieldError = (FieldError) error;
joiner.add(fieldError.getField() + " " + error.getDefaultMessage());
});
return ResponseResult.error(joiner.toString());
} else {
return ResponseResult.error("操作失败");
}
}
log
spring boot 的默认使用的日志是logback
,web模块依赖的有日志 starter
,所以这里不用再引入依赖,详细配置
修改日志级别
Actuator 组件提供了日志相关接口,可以查询日志级别或者动态修改日志级别
// 查看所有包/类的日志级别
/actuator/loggers
// 查看指定包/类日志级别 get 请求
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
//修改日志级别 post 请求 参数 {"configuredLevel":"debug"}
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
日志切面
添加一个日志切面,方便记录方法执行的入参和出参
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAspect {
/**
* 日志描述
*/
String value() default "";
/**
* 日志级别
*/
String level() default "INFO";
}
使用时直接添加到方法上即可
swagger
swagger 是一个很好用的文档生成工具
maven 依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
相关配置
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enable:false}")
private boolean swaggerEnable;
//文档访问前缀
public static final String ACCESS_PREFIX = "/swagger-resources/**,/swagger-ui.html**,/webjars/**,/v2/**";
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// 设置是否开启swagger,生产环境关闭
.enable(swaggerEnable)
.select()
// 当前包路径
.apis(RequestHandlerSelectors.basePackage("com.zhaoguhong.baymax"))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
// 构建api文档的详细信息
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 页面标题
.title("接口文档")
// 创建人
.contact(new Contact("孤鸿", "https://github.com/zhaoguhong/baymax", ""))
// 版本号
.version("1.0")
// 描述
.description("大白的接口文档")
.build();
}
}
为文档加一个开关
#是否开启swagger文档,生产环境关闭
swagger.enable=true
然后就可以愉快的在写代码的同时写文档了
@PostMapping("/add")
@ApiOperation(value = "新增 demo")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}
@ApiModel("示例")
public class Demo extends BaseEntity{
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("标题")
private String title;
@ApiModelProperty("年龄")
private Integer age;
}
访问 localhost:8080/swagger-ui.html
就可以看到效果了
数据库连接池
springboot1.X的数据库连接池是tomcat连接池,springboot2默认的数据库连接池由Tomcat换成 HikariCP,HikariCP
是一个高性能的JDBC连接池,号称最快的连接池
Druid 是阿里巴巴数据库事业部出品,为监控而生的数据库连接池,这里选取Druid
作为项目的数据库连接池
maven 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
设置用户名密码
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456
然后就可以访问 localhost:8080/druid
看监控信息了
spring jdbc
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring对jdbc做了封装和抽象,最常用的是 jdbcTemplate
和 NamedParameterJdbcTemplate
两个类,前者使用占位符,后者使用命名参数,我在jdbcDao
做了一层简单的封装,提供统一的对外接口
jdbcDao
主要方法如下:
// 占位符
find(String sql, Object... args)
// 占位符,手动指定映射mapper
find(String sql, Object[] args, RowMapper<T> rowMapper)
// 命名参数
find(String sql, Map<String, ?> paramMap)
// 命名参数,手动指定映射mapper
find(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
//springjdbc 原queryForMap方法,如果没查询到会抛异常,此处如果没有查询到,返回null
queryForMap(String sql, Object... args)
queryForMap(String sql, Map<String, ?> paramMap)
// 分页查询
find(Page<T> page, String sql, Map<String, ?> parameters, RowMapper<?> mapper)
// 分页查询
find(Page<T> page, String sql, RowMapper<T> mapper, Object... args)
jpa
jpa
是 java
持久化的标准,spring data jpa
使操作数据库变得更方便,需要说明的 spring data jpa
本身并不是jpa的实现,它默认使用的 provider
是 hibernate
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
我把通用的方法抽取了出来了,封装了一个BaseRepository,使用时,直接继承该接口即可
public interface DemoRepository extends BaseRepository<Demo> {
}
BaseRepository
主要方法如下
// 新增,会对创建时间,创建人自动赋值
void saveEntity(T entity)
// 更新,会对更新时间,更新人自动赋值
void updateEntity(T entity)
// 逻辑删除
void deleteEntity(T entity)
// 批量保存
void saveEntites(Collection<T> entitys)
// 批量更新
void updateEntites(Collection<T> entitys)
// 批量逻辑删除
void deleteEntites(Collection<T> entitys)
// 根据id获取实体,会过滤掉逻辑删除的
T getById(Long id)
如果想使用传统的sql形式,可以直接使用JpaDao,为了方便使用,我尽量使JpaDao和JdbcDao的接口保持统一
JpaDao
主要方法如下
// 占位符 例如:from Demo where id =?
find(String sql, Object... args)
// 命名参数
find(String sql, Map<String, ?> paramMap)
// 分页
find(Page<T> page, String hql, Map<String, ?> parameters)
// 分页
find(Page<T> page, String hql, Object... parameters)
redis
Redis 是性能极佳key-value数据库,常用来做缓存,
java 中常用的客户端有 Jedis
和 Lettuce
, spring data redis
是基于 Lettuce
做的二次封装
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
为了在redis读起来更方便,更改序列化方式
@Configuration
public class RedisConfig {
/**
* 设置序列化方式
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 将类名称序列化到json串中
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<Object>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
}
spring cache
spring cache 抽象出了一组缓存接口,通过注解的方式使用,可以很方便的配置其具体实现,详细配置
这里使用redis做为缓存 provider, 默认的value序列化方式是JDK,为方便查看,可以修改为用json序列化
有时会有设置redis key
前缀的需求,默认是这样的
static CacheKeyPrefix simple() {
// 在 cacheName 后面添加 "::"
return name -> name + "::";
}
spring boot 提供的有配置前缀的属性
spring.cache.redis.key-prefix= # Key prefix.
但这是一个坑,这样写的效果实际这样的,会把cacheName
干掉,显然不是我们想要的
CacheKeyPrefix cacheKeyPrefix = (cacheName) -> prefix;
我们想要的是把前缀加在最前面,保留cacheName
CacheKeyPrefix cacheKeyPrefix = (cacheName) -> keyPrefix + "::" + cacheName + "::";
参考org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
,声明 RedisCacheManager
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
public class SpringCacheConfig {
@Autowired
private CacheProperties cacheProperties;
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration());
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return builder.build();
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration() {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 修改序列化为json
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
// 重写前缀拼接方式
config = config.computePrefixWith((cacheName) -> redisProperties.getKeyPrefix() + "::" + cacheName + "::");
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
// 省略 jackson2JsonRedisSerializer()
}
mogodb
MongoDB 是文档型数据库,使用 spring data mogodb
可以很方便对mogodb进行操作
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
sprign data mogodb
提供了 MongoTemplate
对mogodb进行操作,我在该类的基础上又扩展了一下,可以自定义自己的方法
@Configuration
public class MongoDbConfig {
/**
* 扩展自己的mogoTemplate
*/
@Bean
public MyMongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory,
MongoConverter converter) {
return new MyMongoTemplate(mongoDbFactory, converter);
}
}
我扩展了一个分页的方法,可以根据自己的情况扩展其它方法
// mogodb 分页
public <T> Page<T> find(Page<T> page, Query query, Class<T> entityClass)
mybatis
maven 依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
常用配置如下
# 配置文件位置 classpath后面要加*,不然后面通配符不管用
mybatis.mapperLocations=classpath*:com/zhaoguhong/baymax/*/mapper/*Mapper.xml
# 开启驼峰命名自动映射
mybatis.configuration.map-underscore-to-camel-case=true
dao层直接用接口,简洁,方便
@Mapper
public interface DemoMapper {
/**
* 注解方式
*/
@Select("SELECT * FROM demo WHERE user_name = #{userName}")
List<Demo> findByUserName(@Param("userName") String userName);
/**
* xml方式
*/
List<Demo> getDemos();
}
需要注意,xml的namespace必须是mapper类的全限定名,这样才可以建立dao接口与xml的关系
<mapper namespace="com.zhaoguhong.baymax.demo.dao.DemoMapper">
<select id="getDemos" resultType="com.zhaoguhong.baymax.demo.entity.Demo">
select * from demo
</select>
</mapper>
通用mapper
mybatis 的单表增删改查写起来很啰嗦,通用mapper很好的解决了这个问题
maven 依赖
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
常用配置如下
# 通用mapper 多个接口时用逗号隔开
mapper.mappers=com.zhaoguhong.baymax.mybatis.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL
定义自己的 MyMapper
方便扩展,MyMapper
接口 中封装了通用的方法,和jpa
的BaseRepository
类似,这里不再赘述
声明mapper
需要加Mapper
注解,还稍显麻烦,可以用扫描的方式
@Configuration
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
public class MybatisConfig {
}
使用时直接继承MyMapper接口即可
public interface DemoMapper extends MyMapper<Demo>{
}
分页
pagehelper是一个很好用的mybatis的分页插件
maven 依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
常用配置如下
#pagehelper
#指定数据库类型
pagehelper.helperDialect=mysql
#分页合理化参数
pagehelper.reasonable=true
使用分页
PageHelper.startPage(1, 5);
List<Demo> demos = demoMapper.selectAll();
pagehelper 还有好多玩法,可以参考这里
自定义分页
pagehelper 虽然好用,但项目中有自己的分页对象,所以单独写一个拦截器,把他们整合到一起 这个地方要特别注意插件的顺序不要搞错
@Configuration
// 设置mapper扫描的包
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
@Slf4j
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
/**
* 添加自定义的分页插件,pageHelper 的分页插件PageInterceptor是用@PostConstruct添加的,自定义的应该在其后面添加
* 真正执行时顺序是反过来,先执行MyPageInterceptor,再执行 PageInterceptor
*
* 所以要保证 PageHelperAutoConfiguration 先执行
*/
@Autowired
public void addPageInterceptor(PageHelperAutoConfiguration pageHelperAutoConfiguration) {
MyPageInterceptor interceptor = new MyPageInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
log.info("注册自定义分页插件成功");
}
}
}
在使用时只需要传入自定义的分页对象即可
Page<Demo> page = new Page<>(1, 10);
demos = demoMapper.getDemos(page);
spring security
安全模块是项目中必不可少的一环,常用的安全框架有shiro
和spring security
,shiro相对轻量级,使用非常灵活,spring security
相对功能更完善,而且可以和spring 无缝衔接。这里选取spring security
做为安全框架
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
继承WebSecurityConfigurerAdapter类就可以进行配置了
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 设置可以匿名访问的url
.antMatchers(securityProperties.getAnonymousArray()).permitAll()
// 其它所有请求都要认证
.anyRequest().authenticated()
.and()
.formLogin()
// 自定义登录页
.loginPage(securityProperties.getLoginPage())
// 自定义登录请求路径
.loginProcessingUrl(securityProperties.getLoginProcessingUrl())
.permitAll()
.and()
.logout()
.permitAll();
// 禁用CSRF
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
String[] ignoringArray = securityProperties.getIgnoringArray();
// 忽略的资源,直接跳过spring security权限校验
if (ArrayUtils.isNotEmpty(ignoringArray)) {
web.ignoring().antMatchers(ignoringArray);
}
}
/**
*
* 声明密码加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService)
// 配置密码加密方式,也可以不指定,默认就是BCryptPasswordEncoder
.passwordEncoder(passwordEncoder());
}
}
实现UserDetailsService
接口,定义自己的UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsernameAndIsDeleted(username, SystemConstants.UN_DELETED);
if (user == null) {
throw new UsernameNotFoundException("username Not Found");
}
return user;
}
}
配置项
#匿名访问的url,多个用逗号分隔
security.anonymous=/test
#忽略的资源,直接跳过spring security权限校验,一般是用做静态资源,多个用逗号分隔
security.ignoring=/static/**,/images/**
#自定义登录页面
security.loginPage=/login.html
#自定义登录请求路径
security.loginProcessingUrl=/login
项目上下文
为了方面使用,封装一个上下文对象 ContextHolder
// 获取当前线程HttpServletRequest
getRequest()
// 获取当前线程HttpServletResponse
getResponse()
// 获取当前HttpSession
getHttpSession()
setSessionAttribute(String key, Serializable entity)
getSessionAttribute(String key)
setRequestAttribute(String key, Object entity)
getRequestAttribute(String key)
// 获取 ApplicationContext
getApplicationContext()
//根据beanId获取spring bean
getBean(String beanId)
// 获取当前登录用户
getLoginUser()
// 获取当前登录用户 id
getLoginUserId()
// 获取当前登录用户 为空则抛出异常
getRequiredLoginUser()
// 获取当前登录用户id, 为空则抛出异常
getRequiredLoginUserId()
单点登录
单点登录系统(SSO,single sign-on)指的的,多个系统,共用一套用户体系,只要登录其中一个系统,访问其他系统不需要重新登录
CAS
CAS(Central Authentication Service)是耶鲁大学的一个开源项目,是比较流行的单独登录解决方案。在CAS中,只负责登录的系统被称为服务端,其它所有系统被称为客户端
登录流程
- 用户访问客户端,客户端判断是否登录,如果没有登录,重定向到服务端去登录
- 服务端登录成功,带着ticket重定向到客户端
- 客户端拿着ticket发送请求到服务端换取用户信息,获取到后就表示登录成功
登出流程
跳转到sso认证中心进行统一登出,cas 会通知所有客户端进行登出
spring security 整合 cas
maven 依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
spring security 对 cas 做了很好的封装,在使用的过程中,只需要定义好对应的登录fifter和登出fifter即可,整合cas的代码我写在了WebSecurityConfig
类中
相关属性配置
#是否开启单点登录
cas.enable = true
#服务端地址
cas.serverUrl=
#客户端地址
cas.clientUrl=
#登录地址
cas.loginUrl=${cas.serverUrl}/login
#服务端登出地址
cas.serverLogoutUrl=${cas.serverUrl}/logout
#单点登录成功回调地址
cas.clientCasUrl=${cas.clientUrl}/login/cas
邮件
因为要使用freeMarker解析模板,所以也要引入freeMarker依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
相关配置
#邮件
#设置邮箱主机,163邮箱为smtp.163.com,qq为smtp.qq.com
spring.mail.host = smtp.163.com
spring.mail.username =
#授权码
spring.mail.password =
#默认的邮件发送人
mail.sender =
封装一个 MailService
// 根据相关配置发送邮件
void sendMail(MailModel mailModel);
// 发送简单的邮件
void sendSimleMail(String to, String subject, String content);
// 发送html格式的邮件
void sendHtmlMail(String to, String subject, String content);
// 发送带附件的邮件
void sendAttachmentMail(String to, String subject, String content, String path);
// 发送带附件的html格式邮件
void sendAttachmentHtmlMail(String to, String subject, String content, String path);
// 根据模版发送简单邮件
void sendMailByTemplate(String to, String subject, String templateName,
Map<String, Object> params);
}
maven
镜像
设置阿里云镜像,加快下载速度 修改 setting.xml,在 mirrors 节点上,添加
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
也可以在项目 pom.xml 文件添加 ,仅当前项目有效
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
总结
-
spring boot 遵循开箱即用的原则,并不需要做过多配置,网上的教程质量参差不齐,并且1.X和2.X使用时还有诸多不同,因此在使用时尽量参考官方文档
-
有时候默认的配置并不能满足我们的需求,需要做一些自定义配置,推荐先看一下
springboot
自动配置的源码,再做定制化处理 -
技术没有银弹,在做技术选型时不要过于迷信一种技术,适合自己的业务的技术才是最好的